diff --git a/applet/src/cross-applet-main.ts b/applet/src/cross-applet-main.ts index f4a9e96..8683e26 100644 --- a/applet/src/cross-applet-main.ts +++ b/applet/src/cross-applet-main.ts @@ -67,7 +67,8 @@ export class CrossAppletMain extends LitElement { >
diff --git a/applet/src/index.ts b/applet/src/index.ts index 2e6e3b1..39f4ce6 100644 --- a/applet/src/index.ts +++ b/applet/src/index.ts @@ -23,6 +23,7 @@ import { import "@lightningrodlabs/notebooks/dist/elements/all-notes.js"; import "@lightningrodlabs/notebooks/dist/elements/column-header.js"; +import "@lightningrodlabs/notebooks/dist/elements/richtext-note.js"; import "@lightningrodlabs/notebooks/dist/elements/markdown-note.js"; import "@holochain-open-dev/profiles/dist/elements/profiles-context.js"; import "@theweave/api/dist/elements/we-services-context.js"; @@ -50,7 +51,7 @@ function wrapAppletView( weServices: WeServices, innerTemplate: TemplateResult ): TemplateResult { - const synStore = new SynStore(new SynClient(client, "notebooks")); + const synStore = new SynStore(new SynClient(client, "notebooks"), true); return html` = 10" }, "optionalDependencies": { - "@holochain/hc-spin-rust-utils-darwin-arm64": "0.600.0-dev.0", - "@holochain/hc-spin-rust-utils-darwin-x64": "0.600.0-dev.0", - "@holochain/hc-spin-rust-utils-linux-arm64-gnu": "0.600.0-dev.0", - "@holochain/hc-spin-rust-utils-linux-x64-gnu": "0.600.0-dev.0", - "@holochain/hc-spin-rust-utils-win32-x64-msvc": "0.600.0-dev.0" + "@holochain/hc-spin-rust-utils-darwin-arm64": "0.600.0", + "@holochain/hc-spin-rust-utils-darwin-x64": "0.600.0", + "@holochain/hc-spin-rust-utils-linux-arm64-gnu": "0.600.0", + "@holochain/hc-spin-rust-utils-linux-x64-gnu": "0.600.0", + "@holochain/hc-spin-rust-utils-win32-x64-msvc": "0.600.0" } }, "node_modules/@holochain/hc-spin-rust-utils-darwin-arm64": { - "version": "0.600.0-dev.0", - "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-darwin-arm64/-/hc-spin-rust-utils-darwin-arm64-0.600.0-dev.0.tgz", - "integrity": "sha512-34OJXcklXIesQJDJm/0ibpRyxmlfZk5qxmJdFPWrRyyEsD/WeQjXpV8C3p/wLXgegKlJlfsXkqshLidWm5fpzg==", + "version": "0.600.0", + "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-darwin-arm64/-/hc-spin-rust-utils-darwin-arm64-0.600.0.tgz", + "integrity": "sha512-tvU5vBHkkGRSD9K+3HWkTySX7XK+vdD35x3x/4pQ5J/31gRMKcRbrP0W1n7mTOyFUWQjgNHnLJeTM3yt2a7tbA==", "cpu": [ "arm64" ], @@ -2713,9 +2737,9 @@ } }, "node_modules/@holochain/hc-spin-rust-utils-darwin-x64": { - "version": "0.600.0-dev.0", - "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-darwin-x64/-/hc-spin-rust-utils-darwin-x64-0.600.0-dev.0.tgz", - "integrity": "sha512-t4SyQpmGOeH17Ojtlr8aU9K4LApS90Fc0jizbwPIO7emUnRPqD6WGwTj008cE6p/P6dNEX6hTojDS7DL4AxKUg==", + "version": "0.600.0", + "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-darwin-x64/-/hc-spin-rust-utils-darwin-x64-0.600.0.tgz", + "integrity": "sha512-Mewfh8JC49/kXMRNT4Y+ObwrjNk0FTN9aOrStsmRVXfvb4naCRnuExfz3X8iMB85b6SBPxc0lyCC9A4YGvUFPw==", "cpu": [ "x64" ], @@ -2730,9 +2754,9 @@ } }, "node_modules/@holochain/hc-spin-rust-utils-linux-arm64-gnu": { - "version": "0.600.0-dev.0", - "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-linux-arm64-gnu/-/hc-spin-rust-utils-linux-arm64-gnu-0.600.0-dev.0.tgz", - "integrity": "sha512-jiUOD+mwWz+nOjZ89sOREEUHq5BR8DKAqidjGUaTHJNVecS6CrqCtFNmETFHSqu0vc30d3j8nk/HuH2u0hcsBg==", + "version": "0.600.0", + "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-linux-arm64-gnu/-/hc-spin-rust-utils-linux-arm64-gnu-0.600.0.tgz", + "integrity": "sha512-o/RGxy+2hvvN5S8OnVnOLQyx9uiwT2tBSnGbyYZHkZTzipi9eVczOlLlNQnck3s3ApxwAv+FP7dDLJBVxZjmRA==", "cpu": [ "arm64" ], @@ -2747,9 +2771,9 @@ } }, "node_modules/@holochain/hc-spin-rust-utils-linux-x64-gnu": { - "version": "0.600.0-dev.0", - "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-linux-x64-gnu/-/hc-spin-rust-utils-linux-x64-gnu-0.600.0-dev.0.tgz", - "integrity": "sha512-xnqrsMrMC6sina3t4QjlhKat7iycHIwZlN5qtfkwNf59Xwp5s5il1qb2+0iSmAF/RkQUXt7OwpY2cIPsjxZyJQ==", + "version": "0.600.0", + "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-linux-x64-gnu/-/hc-spin-rust-utils-linux-x64-gnu-0.600.0.tgz", + "integrity": "sha512-nr2prm4tlB3inRnQANc6W2M/8JleBTWbleSJ4tdFmS0U2xjoKBLGYmp6TjeTwLAod/99qCNiYoVFblQhxWcZ4w==", "cpu": [ "x64" ], @@ -2764,9 +2788,9 @@ } }, "node_modules/@holochain/hc-spin-rust-utils-win32-x64-msvc": { - "version": "0.600.0-dev.0", - "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-win32-x64-msvc/-/hc-spin-rust-utils-win32-x64-msvc-0.600.0-dev.0.tgz", - "integrity": "sha512-Md98vsYERC84Z2alST35xBB0F2ji/IeggtvwS1+96/5EJcxDagos2Y0aallFuzYvqv6ei1Os7KojOVeHEVy/3Q==", + "version": "0.600.0", + "resolved": "https://registry.npmjs.org/@holochain/hc-spin-rust-utils-win32-x64-msvc/-/hc-spin-rust-utils-win32-x64-msvc-0.600.0.tgz", + "integrity": "sha512-RRyyN+51o7E8TuaKtS3xTAOyh2vVa39dhvY7Tr9EQLxSI9EO1xn4nM+3ZYqSCKPiSUlZ44DwOSrwgmbm2o/9xg==", "cpu": [ "x64" ], @@ -4169,20 +4193,20 @@ } }, "node_modules/@theweave/cli": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@theweave/cli/-/cli-0.15.2.tgz", - "integrity": "sha512-DwDlA7HJJ6Oo4L6RKEdNkjVbGawhM2k9tWuKBvWlWJotth6V1L9hUquTkgOjjVn1svR0CHw3Yh1caKoFytQNLw==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@theweave/cli/-/cli-0.15.5.tgz", + "integrity": "sha512-TXhipxynV4HgVPvZ/g4GLKRm5Tf/vAHhzSDJzJDHYiPe+DY5rLq160I6aU/7KM6SkbODfToL4nTuVVmktYsk4Q==", "dev": true, "hasInstallScript": true, "license": "CAL-1.0", "dependencies": { "@electron-toolkit/preload": "^2.0.0", "@electron-toolkit/utils": "^2.0.0", - "@holochain-open-dev/elements": "^0.600.0-dev.0", - "@holochain-open-dev/profiles": "^0.600.0-dev.2", - "@holochain-open-dev/stores": "^0.600.0-dev.0", - "@holochain-open-dev/utils": "^0.600.0-dev.0", - "@holochain/client": "^0.20.0-dev.2", + "@holochain-open-dev/elements": "^0.600.0", + "@holochain-open-dev/profiles": "^0.600.0", + "@holochain-open-dev/stores": "^0.600.0", + "@holochain-open-dev/utils": "^0.600.0", + "@holochain/client": "^0.20.0", "@lightningrodlabs/we-rust-utils": "^0.600.0-dev.0", "@matthme/electron-updater": "6.3.0-alpha.1", "@msgpack/msgpack": "^2.8.0", @@ -4478,6 +4502,12 @@ "@types/node": "*" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", @@ -4495,6 +4525,22 @@ "@types/lodash": "*" } }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -5102,7 +5148,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -6318,6 +6363,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -9863,6 +9914,15 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/lint-staged": { "version": "10.5.4", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.5.4.tgz", @@ -10260,6 +10320,41 @@ "dev": true, "license": "ISC" }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-it/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -10736,6 +10831,12 @@ "node": ">= 0.8.0" } }, + "node_modules/ordered-map": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ordered-map/-/ordered-map-0.1.0.tgz", + "integrity": "sha512-ttpssyEqKXIeTapKeFTFG+oUCnrC+Fmyy1DPjys0vPJtccBE3baKDFTbJPDuKd9/XHbnKKe+KPQKTzBLri6Mtw==", + "license": "MIT" + }, "node_modules/orderedmap": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", @@ -11291,6 +11392,15 @@ "node": ">=0.4.0" } }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, "node_modules/prosemirror-commands": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", @@ -11302,6 +11412,68 @@ "prosemirror-transform": "^1.10.2" } }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-example-setup": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.2.3.tgz", + "integrity": "sha512-+hXZi8+xbFvYM465zZH3rdZ9w7EguVKmUYwYLZjIJIjPK+I0nPTwn8j0ByW2avchVczRwZmOJGNvehblyIerSQ==", + "license": "MIT", + "dependencies": { + "prosemirror-commands": "^1.0.0", + "prosemirror-dropcursor": "^1.0.0", + "prosemirror-gapcursor": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-inputrules": "^1.0.0", + "prosemirror-keymap": "^1.0.0", + "prosemirror-menu": "^1.0.0", + "prosemirror-schema-list": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", + "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, "node_modules/prosemirror-keymap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", @@ -11312,6 +11484,29 @@ "w3c-keyname": "^2.2.0" } }, + "node_modules/prosemirror-markdown": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-model": { "version": "1.25.4", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", @@ -11321,6 +11516,26 @@ "orderedmap": "^2.0.0" } }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, "node_modules/prosemirror-state": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", @@ -11373,6 +11588,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pupa": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", @@ -12035,6 +12259,12 @@ "dev": true, "license": "MIT" }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13400,6 +13630,12 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -14734,18 +14970,19 @@ }, "ui": { "name": "app", - "version": "0.6.1", + "version": "0.6.3", "license": "MIT", "dependencies": { "@automerge/automerge": "^3.2.0", + "@automerge/prosemirror": "^0.1.0", "@gitgraph/js": "^1.4.0", - "@holochain-open-dev/elements": "^0.600.0-dev.0", - "@holochain-open-dev/profiles": "^0.600.0-dev.2", - "@holochain-open-dev/stores": "^0.600.0-dev.0", - "@holochain-open-dev/utils": "^0.600.0-dev.0", - "@holochain-syn/core": "^0.600.0-rc.2", - "@holochain-syn/text-editor": "^0.600.0-rc.2", - "@holochain/client": "^0.20.0-dev.2", + "@holochain-open-dev/elements": "^0.600.0", + "@holochain-open-dev/profiles": "^0.600.0", + "@holochain-open-dev/stores": "^0.600.0", + "@holochain-open-dev/utils": "^0.600.0", + "@holochain-syn/core": "^0.600.0", + "@holochain-syn/text-editor": "^0.600.0", + "@holochain/client": "^0.20.0", "@lit/context": "^1.0.0", "@lit/localize": "^0.12.0", "@mdi/js": "^7.2.96", @@ -14760,6 +14997,16 @@ "highlight.js": "^11.9.0", "lit": "^3.0.0", "lodash-es": "^4.17.21", + "prosemirror-commands": "^1.5.2", + "prosemirror-example-setup": "^1.1.2", + "prosemirror-history": "^1.3.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.10.1", + "prosemirror-model": "^1.18.0", + "prosemirror-schema-basic": "^1.1.2", + "prosemirror-schema-list": "^1.1.5", + "prosemirror-state": "^1.4.4", + "prosemirror-view": "^1.41.3", "sanitize-filename": "1.6.3" }, "devDependencies": { diff --git a/package.json b/package.json index 3bd65b9..7c49801 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "weave-hash": "weave hash-webhapp 0.6.0 ./workdir/notebooks.webhapp" }, "devDependencies": { - "@theweave/cli": "0.15.2", - "@holochain/hc-spin": "^0.600.0-dev.0", + "@theweave/cli": "0.15.5", + "@holochain/hc-spin": "^0.600.0", "@holochain-playground/cli": "^0.300.1", "bestzip": "^2.2.0", "concurrently": "^6.2.1", diff --git a/ui/index.html b/ui/index.html index 2de3ba8..fd84c58 100644 --- a/ui/index.html +++ b/ui/index.html @@ -22,6 +22,60 @@ display: flex; } + .ProseMirror-prompt { + background: white; + padding: 5px 10px 5px 15px; + border: 1px solid silver; + position: fixed; + border-radius: 3px; + z-index: 11; + box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + } + + .ProseMirror-prompt h5 { + margin: 0; + font-weight: normal; + font-size: 100%; + color: #444; + } + + .ProseMirror-prompt input[type="text"], + .ProseMirror-prompt textarea { + background: #eee; + border: none; + outline: none; + } + + .ProseMirror-prompt input[type="text"] { + padding: 0 4px; + } + + .ProseMirror-prompt-close { + position: absolute; + left: 2px; top: 1px; + color: #666; + border: none; background: transparent; padding: 0; + } + + .ProseMirror-prompt-close:after { + content: "✕"; + font-size: 12px; + } + + .ProseMirror-invalid { + background: #ffc; + border: 1px solid #cc7; + border-radius: 4px; + padding: 5px 10px; + position: absolute; + min-width: 10em; + } + + .ProseMirror-prompt-buttons { + margin-top: 5px; + text-align: center + } + :root { --mdc-theme-primary: #7b1fa2; --mdc-theme-secondary: #ff5722; diff --git a/ui/package.json b/ui/package.json index 1e69210..dbe2dc6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,7 +3,7 @@ "description": "Notebooks UI", "license": "MIT", "author": "dev@holochain.org", - "version": "0.6.1", + "version": "0.6.6", "dnaVersion": "0.6.0", "scripts": { "start": "vite --port $UI_PORT --clearScreen false", @@ -13,13 +13,13 @@ "format": "eslint --ext .ts,.html . --fix && prettier \"**/*.ts\" --write" }, "dependencies": { - "@holochain-open-dev/elements": "^0.600.0-dev.0", - "@holochain-open-dev/profiles": "^0.600.0-dev.2", - "@holochain-open-dev/stores": "^0.600.0-dev.0", - "@holochain-open-dev/utils": "^0.600.0-dev.0", - "@holochain/client": "^0.20.0-dev.2", - "@holochain-syn/core": "^0.600.0-rc.2", - "@holochain-syn/text-editor": "^0.600.0-rc.2", + "@holochain-open-dev/elements": "^0.600.0", + "@holochain-open-dev/profiles": "^0.600.0", + "@holochain-open-dev/stores": "^0.600.0", + "@holochain-open-dev/utils": "^0.600.0", + "@holochain/client": "^0.20.0", + "@holochain-syn/core": "^0.600.0", + "@holochain-syn/text-editor": "^0.600.0", "@theweave/api": "^0.6.0", "@theweave/elements": "^0.6.0", "@lit/context": "^1.0.0", @@ -36,7 +36,18 @@ "@automerge/automerge": "^3.2.0", "@vanillawc/wc-codemirror": "^2.1.0", "@gitgraph/js": "^1.4.0", - "@scoped-elements/cytoscape": "^0.2.0" + "@scoped-elements/cytoscape": "^0.2.0", + "prosemirror-state":"^1.4.4", + "prosemirror-view": "^1.41.3", + "prosemirror-model": "^1.18.0", + "prosemirror-schema-basic": "^1.1.2", + "prosemirror-schema-list": "^1.1.5", + "prosemirror-example-setup": "^1.1.2", + "prosemirror-keymap": "^1.2.2", + "prosemirror-commands": "^1.5.2", + "prosemirror-history": "^1.3.0", + "prosemirror-markdown": "^1.10.1", + "@automerge/prosemirror": "^0.1.0" }, "devDependencies": { "@babel/preset-env": "^7.15.0", diff --git a/ui/src/elements/all-notes.ts b/ui/src/elements/all-notes.ts index ce123cb..0d118ef 100644 --- a/ui/src/elements/all-notes.ts +++ b/ui/src/elements/all-notes.ts @@ -34,6 +34,7 @@ enum SortColumn { Created, Modified, Title, + Style, Author } @@ -45,6 +46,7 @@ type NoteRow = { actionHash: ActionHash, state: TextEditorState, authorSort: number, + style: string, archived: boolean } @@ -143,6 +145,7 @@ export class AllNotes extends LitElement { actionHash:note.document.actionHash, state: note.latestState, authorSort, + style: (decode(note.document.entry.meta!) as any).editorType, archived } } @@ -183,6 +186,10 @@ export class AllNotes extends LitElement { if (this.sortDirection=== SortDirection.Descending) return notes.sort((a, b) => b.authorSort - a.authorSort) return notes.sort((a, b) => a.authorSort - b.authorSort) + case SortColumn.Style: + if (this.sortDirection=== SortDirection.Descending) + return notes.sort((a, b) => b.style.localeCompare(a.style)) + return notes.sort((a, b) => a.style.localeCompare(b.style)) } return notes } @@ -235,6 +242,9 @@ export class AllNotes extends LitElement { ${createDate.toLocaleDateString()} ${createDate.toLocaleTimeString()} + + ${note.style === "richtext" ? msg("Rich Text") : msg("Markdown")} + @@ -321,6 +331,13 @@ export class AllNotes extends LitElement { >${msg("Created")} + + this.setSort(SortColumn.Style, e.detail.direction)}> + ${msg("Style")} + +
- - ${this._view === View.Both || this._view === View.Edit ? html` -
+ +
-
` : ""} +
- ${this._view === View.Both || this._view === View.View ? html` -
+
@@ -597,7 +595,8 @@ export class MarkdownNote extends LitElement {
-
` : ""} +
+
`; diff --git a/ui/src/elements/richtext-note.ts b/ui/src/elements/richtext-note.ts new file mode 100644 index 0000000..cf1c078 --- /dev/null +++ b/ui/src/elements/richtext-note.ts @@ -0,0 +1,672 @@ +import { EntryRecord } from "@holochain-open-dev/utils"; +import { consume } from "@lit/context"; +import { css, html, LitElement, PropertyValueMap } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import { + Workspace, + DocumentStore, + WorkspaceStore, + stateFromCommit, + SynStore, + synContext, + synDocumentContext, + SessionStore, + SynConfig, +} from "@holochain-syn/core"; + +import "@holochain-syn/core/dist/elements/syn-context.js"; +import "@holochain-syn/core/dist/elements/session-participants.js"; +import "./commit-history"; +import "./diff-viewer"; +import "@shoelace-style/shoelace/dist/components/spinner/spinner.js"; +import "@shoelace-style/shoelace/dist/components/input/input.js"; +import "@shoelace-style/shoelace/dist/components/button/button.js"; +import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js"; +import "@shoelace-style/shoelace/dist/components/button-group/button-group.js"; +import "@shoelace-style/shoelace/dist/components/card/card.js"; +import "@shoelace-style/shoelace/dist/components/dialog/dialog.js"; +import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js"; +import '@shoelace-style/shoelace/dist/components/radio-group/radio-group.js'; +import '@shoelace-style/shoelace/dist/components/radio-button/radio-button.js'; +import "@holochain-open-dev/profiles/dist/elements/agent-avatar.js"; +import "./workspace-list"; +import "@shoelace-style/shoelace/dist/components/badge/badge.js"; +import "@shoelace-style/shoelace/dist/components/drawer/drawer.js"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import "./session-status" +import "./syn-md-editor"; +import "./syn-pm-editor" + +import { Profile, ProfilesStore, profilesStoreContext } from '@holochain-open-dev/profiles'; + + +import { + hashState, + notifyError, + onSubmit, + sharedStyles, + wrapPathInSvg, +} from "@holochain-open-dev/elements"; +import { ActionHash, encodeHashToBase64, EntryHash } from "@holochain/client"; +import { + completed, + joinAsyncMap, + mapAndJoin, + pipe, + StoreSubscriber, + subscribe, +} from "@holochain-open-dev/stores"; +import { SlChangeEvent, SlDialog, SlDrawer, SlRadioGroup } from "@shoelace-style/shoelace"; +import { msg } from "@lit/localize"; +import { decode } from "@msgpack/msgpack"; +import { Marked } from "@ts-stack/markdown"; +import { mdiArrowLeft, mdiBookOpenOutline, mdiEye, mdiPencil, mdiClose, mdiDotsGrid } from "@mdi/js"; +import { isWeaveContext, WAL } from "@theweave/api"; +import { + TextEditorEphemeralState, + TextEditorState, +} from "../grammar"; +import { NoteMeta } from "../types.js"; +import { notebooksContext, NotebooksStore } from "../store"; +import { renderAsyncStatus } from "../utils.js"; + +enum View { + Edit, + Both, + View +} + +interface HistoryTypes { + linear: boolean; + commit: boolean; + workspaces: boolean; +} + +const POCKET_ICON=`` + +const WORKSPACE_NOT_FOUND = "The requested workspace was not found"; + +const SYN_CONFIG: SynConfig = { + hearbeatInterval: 5 * 1000, + newPeersDiscoveryInterval: 30 * 1000, + outOfSessionTimeout: 60 * 1000, + commitStrategy: { CommitEveryNDeltas: 200, CommitEveryNMs: 1000 * 30 }, // TODO: reduce ms +} + +@customElement("richtext-note") +export class RichtextNote extends LitElement { + @consume({ context: synDocumentContext, subscribe: true }) + @property() + documentStore!: DocumentStore; + + @consume({ context: notebooksContext, subscribe: true }) + @property() + notebooksStore!: NotebooksStore; + + @consume({ context: profilesStoreContext, subscribe: true }) + profilesStore!: ProfilesStore; + + @property() + standalone = false + + @state() + _renderDrawer = false + + @state() + _historyTypes: HistoryTypes = { + workspaces: false, + linear: true, + commit: false, + }; + + @state() + _diffView: boolean = false; + + _meta = new StoreSubscriber( + this, + () => + pipe( + this.documentStore.record, + (document) => decode(document.entry.meta!) as NoteMeta + ), + () => [this.documentStore] + ); + + _session = new StoreSubscriber( + this, + () => + pipe( + this.documentStore.allWorkspaces, + (map) => mapAndJoin(map, (w) => w.name), + (allWorkspaces) => { + const workspace: [EntryHash, String] | undefined = Array.from( + allWorkspaces.entries() + ).find(([hash, name]) => name === this._workspaceName); + + if (!workspace) throw new Error(WORKSPACE_NOT_FOUND); + return this.documentStore.workspaces.get(workspace[0]); + }, + (workspaceStore) => workspaceStore.session, + (sessionStore, w) => { + if (sessionStore) { + return sessionStore; + } + + // Only join session if not already joining + if (!this._joiningSession) { + this._joiningSession = true; + console.log("joining session for workspace", this._workspaceName); + + const sessionPromise = w.joinSession(SYN_CONFIG); + sessionPromise.finally(() => { + this._joiningSession = false; + }); + + return sessionPromise; + } + + return undefined; + }, + (s) => s?.state, + (state, sessionStore) => + sessionStore && state ? [sessionStore, state] as [ + SessionStore, + TextEditorState + ] : undefined + ), + () => [this.documentStore, this._workspaceName] + ); + + @state() + _workspaceName: string = "main"; + + @state() + _view: View = View.Both + + @state(hashState()) + _selectedCommitHash: ActionHash | undefined; + + @state() + creatingWorkspace = false; + + @state() + _joiningSession = false; + + async createWorkspace( + name: string, + initialTipHash: EntryHash, + sessionStore: SessionStore + ) { + if (this.creatingWorkspace) return; + + this.creatingWorkspace = true; + + await sessionStore.leaveSession(); + try { + await this.documentStore.createWorkspace(name, initialTipHash); + ( + this.shadowRoot?.getElementById("new-workspace-dialog") as SlDialog + ).hide(); + this._workspaceName = name; + } catch (e) { + notifyError(msg("Error creating the workspace")); + console.error(e); + } + this.creatingWorkspace = false; + } + + renderNewWorkspaceButton( + sessionStore: SessionStore + ) { + return html` { + ( + this.shadowRoot?.getElementById("new-workspace-dialog") as SlDialog + ).show(); + }} + > + ${msg("Create Workspace From This Commit")} + + +
+ this.createWorkspace( + f.name, + this._selectedCommitHash!, + sessionStore + ) + )} + id="new-workspace-form" + > + +
+ + + ( + this.shadowRoot?.getElementById( + "new-workspace-dialog" + ) as SlDialog + ).hide()} + > + ${msg("Cancel")} + + + + ${msg("Create")} + +
`; + } + + renderSelectedCommitDiff() { + if (!this._selectedCommitHash) + return html`
+ ${msg("Select a commit to see its contents")} +
`; + + switch (this._session.value.status) { + case "pending": + return this.renderLoading(); + case "complete": + const sessionValue = this._session.value.value; + if (!sessionValue || !sessionValue[1]) { + return this.renderLoading(); + } + return html` + + `; + case "error": + return html`
+ ${msg("Error loading session for diff")} +
`; + } + } + + renderSelectedCommit() { + if (!this._selectedCommitHash) + return html`
+ ${msg("Select a commit to see its contents")} +
`; + + return html`${subscribe( + this.documentStore.commits.get(this._selectedCommitHash), + renderAsyncStatus({ + complete: (v) => html`
+
+ +
Commit: ${this._selectedCommitHash ? encodeHashToBase64(this._selectedCommitHash):""}
+
+ by ${subscribe(this.profilesStore.profiles.get(v.action.author), + renderAsyncStatus({ + complete: (v) => html` ${v?.entry.nickname}`, + pending: () => this.renderLoading(), + error: (e) => html``, + }) + )} + on ${(new Date(v.action.timestamp)).toLocaleDateString()} ${(new Date(v.action.timestamp)).toLocaleTimeString()} + + +
+ +
+ ${(stateFromCommit(v.entry) as TextEditorState).text.join('')} +
+
+
+
`, + pending: () => this.renderLoading(), + error: (e) => html``, + }) + )}`; + } + + renderVersionControlPanel( + sessionStore: SessionStore + ) { + return html`
+
+
+
+ { + this._historyTypes = { + ...this._historyTypes, + workspaces: !this._historyTypes.workspaces, + } + }} + class=${this._historyTypes.workspaces ? "active" : ""} + > + + ${msg("Workspaces")} + { + this._historyTypes = { + ...this._historyTypes, + linear: !this._historyTypes.linear, + } + }} + class=${this._historyTypes.linear ? "active" : ""} + > + + ${msg("History")} + { + this._historyTypes = { + ...this._historyTypes, + commit: !this._historyTypes.commit, + } + }} + class=${this._historyTypes.commit ? "active" : ""} + > + + ${msg("Commit")} +
+ + { + this._renderDrawer = false; + }} + > +
+ + + ${ this._historyTypes.workspaces ? html` + { + await sessionStore.commitChanges(); + await sessionStore.leaveSession(); + console.log("left session"); + this._joiningSession = false; + this._workspaceName = e.detail.workspaceName; + }} + > + ` : ""} + ${ (this._historyTypes.commit || this._historyTypes.linear) ? html` +
+ + ${ this._historyTypes.linear ? html` +
+ { + this._selectedCommitHash = e.detail.commitHash; + this._historyTypes = { + ...this._historyTypes, + commit: true, + } + }} + > +
+ ` : ""} + ${ this._historyTypes.commit ? html` +
+ +
+ Selected Commit + { + const s:SlRadioGroup = e.target as SlRadioGroup + this._diffView = s.value === "2" + }}> + ${msg("View")} + ${msg("Diff")} + +
+ +
+ ${this._diffView ? html` + ${this.renderSelectedCommitDiff()} + `: html` +
${this.renderNewWorkspaceButton(sessionStore)}
+ ${this.renderSelectedCommit()} + ` } +
+
+ ` : ""} +
+
+ ` : ""} +
+
+
`; + } + + copyWALToClipboard(documentHash: EntryHash) { + const attachment: WAL = { hrl: [this.notebooksStore.dnaHash, documentHash], context: {} } + this.notebooksStore.weaveClient?.assets.assetToPocket(attachment) + } + + renderNoteWorkspace( + sessionStore: SessionStore, + state: TextEditorState + ) { + return html` + + ${this._renderDrawer ? html`` : ""} + ${ this._renderDrawer ? html` +
this._renderDrawer = false}> + ${this.renderVersionControlPanel(sessionStore)}
+ ` : html``} +
+
+ + ${!this.standalone ? html` + { + this.dispatchEvent( + new CustomEvent("close", { + detail: {}, + composed: true, + bubbles: true, + }) + ); + }} + >`:""} + + ${ isWeaveContext() ? html` + { + this.copyWALToClipboard(this.documentStore.documentHash); + }} + > + + `:""} + + + ${msg("Participants:")} + + ${msg("Active Workspace:")} + ${this._workspaceName} + { + this._renderDrawer = !this._renderDrawer; + }} + > + ${msg("Version Control")} + + +
+
+ + ${this._view === View.Both || this._view === View.Edit ? html` +
+
+
+ + + +
+
+
` : ""} +
+
+
+ `; + } + + renderLoading() { + return html` +
+ +
+ `; + } + + renderNoRootFound() { + return html` +
+ ${msg( + "The note was not found. Try again when one of its past contributors is online." + )} +
+ `; + } + + render() { + switch (this._session.value.status) { + case "pending": + return this.renderLoading(); + case "complete": + const sessionValue = this._session.value.value; + if (!sessionValue || !sessionValue[0] || !sessionValue[1]) { + return this.renderLoading(); + } + return this.renderNoteWorkspace( + sessionValue[0], + sessionValue[1] + ); + case "error": + if (this._session.value.error.message === WORKSPACE_NOT_FOUND) + return html`
+ + ${msg("Creating workspace...")} +
`; + return this.renderNoRootFound(); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this._session.value.status === "complete" && this._session.value.value?.[0]) { + this._session.value.value[0].commitChanges(); + this._session.value.value[0].leaveSession(); + console.log("left session on disconnect"); + } + } + + static styles = [ + sharedStyles, + css` + .active::part(base) { + background-color: var(--sl-color-primary-600); + color: white; + } + .active::part(base):hover { + background-color: var(--sl-color-primary-500); + color: white; + } + sl-drawer::part(body) { + display: flex; + } + :host { + display: flex; + flex: 1; + } + .controls { + display: flex; + flex: 1; + flex-wrap: nowrap; + align-items: center; + } + .marked { + display:block; + word-wrap: normal; + } + .CodeMirror-wrap pre { + word-break: break-word; + } + .tooltip::part(popup) { + z-index: 10; + } + + /* loaded from https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css */ + pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-comment,.hljs-quote,.hljs-variable{color:green}.hljs-built_in,.hljs-keyword,.hljs-name,.hljs-selector-tag,.hljs-tag{color:#00f}.hljs-addition,.hljs-attribute,.hljs-literal,.hljs-section,.hljs-string,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type{color:#a31515}.hljs-deletion,.hljs-meta,.hljs-selector-attr,.hljs-selector-pseudo{color:#2b91af}.hljs-doctag{color:grey}.hljs-attr{color:red}.hljs-bullet,.hljs-link,.hljs-symbol{color:#00b0e8}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} + `, + ]; +} \ No newline at end of file diff --git a/ui/src/elements/syn-pm-editor.ts b/ui/src/elements/syn-pm-editor.ts new file mode 100644 index 0000000..43d3d6d --- /dev/null +++ b/ui/src/elements/syn-pm-editor.ts @@ -0,0 +1,1615 @@ +import { LitElement, html, css } from 'lit'; +import { customElement, property, query, state } from 'lit/decorators.js'; +import { SliceStore } from '@holochain-syn/core'; +import { + AgentPubKey, + decodeHashFromBase64, + encodeHashToBase64, +} from '@holochain/client'; +import { derived, StoreSubscriber } from '@holochain-open-dev/stores'; +import { styleMap } from 'lit/directives/style-map.js'; +import './agent-cursor.js'; + +// ProseMirror imports +import { EditorView } from 'prosemirror-view'; +import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state'; +import { Schema, Node as PMNode } from 'prosemirror-model'; +import { schema as basicSchema } from 'prosemirror-schema-basic'; +import { addListNodes } from 'prosemirror-schema-list'; +import { history, undo, redo } from 'prosemirror-history'; +import { keymap } from 'prosemirror-keymap'; +import { exampleSetup } from 'prosemirror-example-setup'; + +import { + AgentSelection, + TextEditorEphemeralState, + TextEditorState, + textEditorGrammar, +} from '../grammar.js'; +import { elemIdToPosition } from '../utils.js'; + +/** + * + * A ProseMirror-based collaborative editor that integrates directly with Holochain Syn. + * Works like syn-md-editor but uses ProseMirror for rich text editing. + */ +@customElement('syn-pm-editor') +export class SynPmEditor extends LitElement { + @property({ type: Object }) + slice!: SliceStore; + + @property({ type: Function}) + doSet = (val: string) => { + this.setPlainTextContent(val); + } + + _state = new StoreSubscriber( + this, + () => this.slice.state, + () => [this.slice] + ); + + _cursors = new StoreSubscriber( + this, + () => this.slice.ephemeral, + () => [this.slice] + ); + + private view: EditorView | null = null; + + private schema: Schema; + + @query('#editor') private editorEl!: HTMLDivElement; + + private isUpdatingFromSyn = false; + + // Cache for the last state text to avoid unnecessary rebuilds + private lastStateText: string = ''; + + // Cache for position mapping between ProseMirror and markdown + private pmToMarkdownMap: Map = new Map(); + + private markdownToPmMap: Map = new Map(); + + @state() + private _ctrlPressCount = 0; + + @state() + private _showSecretButton = false; + + @state() + private _isAutoTyping = false; + + @state() + private _delayedCursors: TextEditorEphemeralState = {}; + + @state() + private _showCursors = true; + + private _ctrlPressTimer: any = null; + + private _cursorUpdateTimer: any = null; + + private _autoTypeInterval: any = null; + + private _loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. '; + + private _loremIndex = 0; + + private _boundHandleKeyDown = this._handleKeyDown.bind(this); + + private _boundHandleKeyUp = this._handleKeyUp.bind(this); + + constructor() { + super(); + // Create schema with lists + const mySchema = new Schema({ + nodes: addListNodes(basicSchema.spec.nodes, 'paragraph block*', 'block'), + marks: basicSchema.spec.marks, + }); + this.schema = mySchema; + + // Debug: log schema nodes + console.log('[Schema] Available nodes:', Object.keys(this.schema.nodes)); + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener('keydown', this._boundHandleKeyDown); + window.addEventListener('keyup', this._boundHandleKeyUp); + } + + firstUpdated() { + this.initializeEditor(); + } + + private initializeEditor() { + const synState = this._state.value; + const initialText = synState ? synState.text.join('') : ''; + + // Create initial doc + const doc = this.createDocFromText(initialText); + + // Create editor state + const state = EditorState.create({ + schema: this.schema, + doc, + plugins: [ + ...exampleSetup({ schema: this.schema }), + keymap({ 'Mod-z': undo, 'Mod-y': redo }), + this.createSynPlugin(), + ], + }); + + // Create view + this.view = new EditorView(this.editorEl, { state }); + + // Subscribe to Syn changes + this.subscribeToSynChanges(); + + setTimeout(() => this.view?.focus(), 100); + } + + private createDocFromText(text: string): PMNode { + // console.log('createDocFromText input:', JSON.stringify(text)); + if (!text) { + return this.schema.node('doc', null, [this.schema.node('paragraph')]); + } + + // Parse lines and group consecutive list items + const lines = text.split('\n'); + const nodes: PMNode[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + let handled = false; + + if (!line) { + // Empty line = empty paragraph + nodes.push(this.schema.node('paragraph')); + i += 1; + } else { + // Check for heading + const headingMatch = line.match(/^(#{1,6})\s(.+)$/); + if (headingMatch) { + const level = headingMatch[1].length; + const content = this.schema.text(headingMatch[2]); + nodes.push(this.schema.node('heading', { level }, content)); + i += 1; + handled = true; + } + + // Check for blockquote - group consecutive lines + if (!handled) { + const quoteMatch = line.match(/^>\s?(.*)$/); + if (quoteMatch) { + const paragraphs: PMNode[] = []; + while (i < lines.length) { + const itemMatch = lines[i].match(/^>\s?(.*)$/); + if (!itemMatch) break; + const content = this.parseInlineFormatting(itemMatch[1]); + paragraphs.push(this.schema.node('paragraph', null, content)); + i += 1; + } + nodes.push(this.schema.node('blockquote', null, paragraphs)); + handled = true; + } + } + + // Check for bullet list - group consecutive items + if (!handled) { + const bulletMatch = line.match(/^[-*]\s(.+)$/); + if (bulletMatch) { + const listItems: PMNode[] = []; + while (i < lines.length) { + const itemMatch = lines[i].match(/^[-*]\s(.+)$/); + if (!itemMatch) break; + const content = this.parseInlineFormatting(itemMatch[1]); + listItems.push(this.schema.node('list_item', null, [ + this.schema.node('paragraph', null, content) + ])); + i += 1; + } + nodes.push(this.schema.node('bullet_list', null, listItems)); + handled = true; + } + } + + // Check for ordered list - group consecutive items + if (!handled) { + const orderedMatch = line.match(/^(\d+)\.\s(.+)$/); + if (orderedMatch) { + const listItems: PMNode[] = []; + const startOrder = parseInt(orderedMatch[1], 10); + while (i < lines.length) { + const itemMatch = lines[i].match(/^\d+\.\s(.+)$/); + if (!itemMatch) break; + const content = this.parseInlineFormatting(itemMatch[1]); + listItems.push(this.schema.node('list_item', null, [ + this.schema.node('paragraph', null, content) + ])); + i += 1; + } + nodes.push(this.schema.node('ordered_list', { order: startOrder }, listItems)); + handled = true; + } + } + + // Check for standalone image on its own line + if (!handled) { + const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + if (imageMatch) { + const alt = imageMatch[1]; + const src = imageMatch[2]; + console.log('[createDocFromText] Parsing standalone image:', { alt, src }); + // Images are inline nodes, so wrap them in a paragraph + const imageNode = this.schema.node('image', { src, alt, title: alt }); + nodes.push(this.schema.node('paragraph', null, [imageNode])); + i += 1; + handled = true; + } + } + + // Regular paragraph + if (!handled) { + const content = this.parseInlineFormatting(line); + nodes.push(this.schema.node('paragraph', null, content)); + i += 1; + } + } + } + + return this.schema.node('doc', null, nodes); + } + + private parseInlineFormatting(text: string): PMNode[] | undefined { + if (!text) return undefined; + + const nodes: PMNode[] = []; + let i = 0; + + while (i < text.length) { + let handled = false; + + // Check for images ![alt](src) + if (!handled && text[i] === '!' && text[i + 1] === '[') { + const closeBracket = text.indexOf(']', i + 2); + if (closeBracket !== -1 && text[closeBracket + 1] === '(') { + const closeParen = text.indexOf(')', closeBracket + 2); + if (closeParen !== -1) { + const alt = text.substring(i + 2, closeBracket); + const src = text.substring(closeBracket + 2, closeParen); + console.log('[parseInlineFormatting] Parsing inline image:', { alt, src }); + nodes.push(this.schema.node('image', { src, alt, title: alt })); + i = closeParen + 1; + handled = true; + } + } + } + + // Check for links [text](url) + if (!handled && text[i] === '[') { + const closeBracket = text.indexOf(']', i + 1); + if (closeBracket !== -1 && text[closeBracket + 1] === '(') { + const closeParen = text.indexOf(')', closeBracket + 2); + if (closeParen !== -1) { + const linkText = text.substring(i + 1, closeBracket); + const href = text.substring(closeBracket + 2, closeParen); + const mark = this.schema.marks.link.create({ href }); + nodes.push(this.schema.text(linkText, [mark])); + i = closeParen + 1; + handled = true; + } + } + } + + // Check for bold+italic (*** or ***) + if (!handled && text.substring(i, i + 3) === '***') { + const end = text.indexOf('***', i + 3); + if (end !== -1) { + const content = text.substring(i + 3, end); + const marks = [this.schema.marks.strong.create(), this.schema.marks.em.create()]; + nodes.push(this.schema.text(content, marks)); + i = end + 3; + handled = true; + } + } + + // Check for bold (** or **) + if (!handled && text.substring(i, i + 2) === '**') { + const end = text.indexOf('**', i + 2); + if (end !== -1) { + const content = text.substring(i + 2, end); + const mark = this.schema.marks.strong.create(); + nodes.push(this.schema.text(content, [mark])); + i = end + 2; + handled = true; + } + } + + // Check for italic (* or *) + if (!handled && text[i] === '*') { + const end = text.indexOf('*', i + 1); + if (end !== -1 && text[end - 1] !== '*' && (end === i + 1 || text[i + 1] !== '*')) { + const content = text.substring(i + 1, end); + const mark = this.schema.marks.em.create(); + nodes.push(this.schema.text(content, [mark])); + i = end + 1; + handled = true; + } + } + + // Check for code (` or `) + if (!handled && text[i] === '`') { + const end = text.indexOf('`', i + 1); + if (end !== -1) { + const content = text.substring(i + 1, end); + const mark = this.schema.marks.code.create(); + nodes.push(this.schema.text(content, [mark])); + i = end + 1; + handled = true; + } + } + + // Plain text - collect until next special character + if (!handled) { + let plainEnd = i + 1; + while (plainEnd < text.length && text[plainEnd] !== '*' && text[plainEnd] !== '`' && text[plainEnd] !== '[') { + plainEnd += 1; + } + nodes.push(this.schema.text(text.substring(i, plainEnd))); + i = plainEnd; + } + } + + return nodes.length > 0 ? nodes : undefined; + } + + private docToText(doc: PMNode): string { + // Custom serialization: use single newlines between paragraphs, not double + const lines: string[] = []; + + doc.forEach((node) => { + if (node.type.name === 'paragraph') { + // Serialize the paragraph content to markdown (for bold, italic, etc.) + let text = ''; + node.forEach((child) => { + if (child.type.name === 'image') { + // Handle images within paragraphs + const alt = child.attrs.alt || ''; + const src = child.attrs.src || ''; + console.log('[docToText] Serializing image:', { alt, src }); + text += `![${alt}](${src})`; + } else if (child.isText) { + let prefix = ''; + let suffix = ''; + + // Check for link mark first + const linkMark = child.marks.find(m => m.type.name === 'link'); + if (linkMark) { + // Link format: [text](url) + prefix = '['; + suffix = `](${linkMark.attrs.href})`; + } + + // Check for both strong and em marks (bold + italic) + const hasStrong = child.marks.some(m => m.type.name === 'strong'); + const hasEm = child.marks.some(m => m.type.name === 'em'); + const hasCode = child.marks.some(m => m.type.name === 'code'); + + if (hasStrong && hasEm) { + // Both bold and italic = *** + prefix += '***'; + suffix = '***' + suffix; + } else if (hasStrong) { + // Just bold = ** + prefix += '**'; + suffix = '**' + suffix; + } else if (hasEm) { + // Just italic = * + prefix += '*'; + suffix = '*' + suffix; + } else if (hasCode) { + // Code = ` + prefix += '`'; + suffix = '`' + suffix; + } + + text += prefix + (child.text || '') + suffix; + } + }); + lines.push(text); // Empty string for empty paragraphs + } else if (node.type.name === 'heading') { + const level = node.attrs.level || 1; + const hashes = '#'.repeat(level); + // Serialize heading content with inline formatting + let text = ''; + node.forEach((child) => { + if (child.isText) { + let prefix = ''; + let suffix = ''; + + // Check for link mark first + const linkMark = child.marks.find(m => m.type.name === 'link'); + if (linkMark) { + prefix = '['; + suffix = `](${linkMark.attrs.href})`; + } + + const hasStrong = child.marks.some(m => m.type.name === 'strong'); + const hasEm = child.marks.some(m => m.type.name === 'em'); + const hasCode = child.marks.some(m => m.type.name === 'code'); + + if (hasStrong && hasEm) { + prefix += '***'; + suffix = '***' + suffix; + } else if (hasStrong) { + prefix += '**'; + suffix = '**' + suffix; + } else if (hasEm) { + prefix += '*'; + suffix = '*' + suffix; + } else if (hasCode) { + prefix += '`'; + suffix = '`' + suffix; + } + + text += prefix + (child.text || '') + suffix; + } + }); + lines.push(`${hashes} ${text}`); + } else if (node.type.name === 'blockquote') { + // Blockquote: serialize each paragraph on its own line with > + node.forEach((child) => { + if (child.type.name === 'paragraph') { + let text = ''; + child.forEach((textNode) => { + if (textNode.isText) { + let prefix = ''; + let suffix = ''; + + // Check for link mark first + const linkMark = textNode.marks.find(m => m.type.name === 'link'); + if (linkMark) { + prefix = '['; + suffix = `](${linkMark.attrs.href})`; + } + + const hasStrong = textNode.marks.some(m => m.type.name === 'strong'); + const hasEm = textNode.marks.some(m => m.type.name === 'em'); + const hasCode = textNode.marks.some(m => m.type.name === 'code'); + + if (hasStrong && hasEm) { + prefix += '***'; + suffix = '***' + suffix; + } else if (hasStrong) { + prefix += '**'; + suffix = '**' + suffix; + } else if (hasEm) { + prefix += '*'; + suffix = '*' + suffix; + } else if (hasCode) { + prefix += '`'; + suffix = '`' + suffix; + } + + text += prefix + (textNode.text || '') + suffix; + } + }); + lines.push(`> ${text}`); + } + }); + } else if (node.type.name === 'bullet_list') { + // Bullet list: each item on its own line with - + node.forEach((listItem) => { + if (listItem.type.name === 'list_item') { + // Serialize the paragraph content inside the list item + let itemText = ''; + listItem.descendants((child) => { + if (child.isText) { + let prefix = ''; + let suffix = ''; + + // Check for link mark first + const linkMark = child.marks.find(m => m.type.name === 'link'); + if (linkMark) { + prefix = '['; + suffix = `](${linkMark.attrs.href})`; + } + + const hasStrong = child.marks.some(m => m.type.name === 'strong'); + const hasEm = child.marks.some(m => m.type.name === 'em'); + const hasCode = child.marks.some(m => m.type.name === 'code'); + + if (hasStrong && hasEm) { + prefix += '***'; + suffix = '***' + suffix; + } else if (hasStrong) { + prefix += '**'; + suffix = '**' + suffix; + } else if (hasEm) { + prefix += '*'; + suffix = '*' + suffix; + } else if (hasCode) { + prefix += '`'; + suffix = '`' + suffix; + } + + itemText += prefix + (child.text || '') + suffix; + } + }); + lines.push(`- ${itemText}`); + } + }); + } else if (node.type.name === 'ordered_list') { + // Ordered list: each item on its own line with number + const order = node.attrs.order || 1; + let itemNum = order; + node.forEach((listItem) => { + if (listItem.type.name === 'list_item') { + // Serialize the paragraph content inside the list item + let itemText = ''; + listItem.descendants((child) => { + if (child.isText) { + let prefix = ''; + let suffix = ''; + + // Check for link mark first + const linkMark = child.marks.find(m => m.type.name === 'link'); + if (linkMark) { + prefix = '['; + suffix = `](${linkMark.attrs.href})`; + } + + child.marks.forEach((mark) => { + if (mark.type.name === 'strong') { + prefix += '**'; + suffix = '**' + suffix; + } else if (mark.type.name === 'em') { + prefix += '*'; + suffix = '*' + suffix; + } else if (mark.type.name === 'code') { + prefix += '`'; + suffix = '`' + suffix; + } + }); + itemText += prefix + (child.text || '') + suffix; + } + }); + lines.push(`${itemNum}. ${itemText}`); + itemNum += 1; + } + }); + } else { + // Fallback for other node types + lines.push(node.textContent); + } + }); + + const markdown = lines.join('\n'); + // console.log('docToText serialized:', JSON.stringify(markdown)); + + // Build position mapping by walking the doc and the markdown in parallel + this.buildPositionMaps(doc, markdown); + + return markdown; + } + + private buildPositionMaps(doc: PMNode, markdown: string) { + this.pmToMarkdownMap.clear(); + this.markdownToPmMap.clear(); + + // Walk through the document and match it to the markdown string + let markdownIndex = 0; + + doc.forEach((node, offset) => { + const nodeStart = offset; + + if (node.type.name === 'heading') { + const level = node.attrs.level || 1; + const prefix = '#'.repeat(level) + ' '; + markdownIndex += prefix.length; // Skip "# " or "## " etc. + + this.mapTextContent(node, nodeStart + 1, markdown, markdownIndex); + markdownIndex += node.textContent.length; + + } else if (node.type.name === 'blockquote') { + markdownIndex += 2; // Skip "> " + + // Blockquote contains a paragraph, so we need to go deeper + node.forEach((child, childOffset) => { + if (child.type.name === 'paragraph') { + this.mapTextContent(child, nodeStart + childOffset + 2, markdown, markdownIndex); + markdownIndex += child.textContent.length; + } + }); + + } else if (node.type.name === 'bullet_list') { + node.forEach((listItem, listItemOffset) => { + if (listItem.type.name === 'list_item') { + markdownIndex += 2; // Skip "- " + + // List item contains a paragraph + listItem.forEach((child, childOffset) => { + if (child.type.name === 'paragraph') { + // PM position = nodeStart (bullet_list) + listItemOffset (list_item) + childOffset (paragraph) + 3 + // +1 for bullet_list open, +1 for list_item open, +1 for paragraph open + const pmPos = nodeStart + listItemOffset + childOffset + 3; + this.mapTextContent(child, pmPos, markdown, markdownIndex); + markdownIndex += child.textContent.length; + } + }); + + // Account for newline after list item + if (markdownIndex < markdown.length && markdown[markdownIndex] === '\n') { + markdownIndex += 1; + } + } + }); + return; // Already handled newline + + } else if (node.type.name === 'ordered_list') { + let itemNum = 1; + node.forEach((listItem, listItemOffset) => { + if (listItem.type.name === 'list_item') { + const prefix = `${itemNum}. `; + markdownIndex += prefix.length; // Skip "1. " or "2. " etc. + + // List item contains a paragraph + listItem.forEach((child, childOffset) => { + if (child.type.name === 'paragraph') { + const pmPos = nodeStart + listItemOffset + childOffset + 3; + this.mapTextContent(child, pmPos, markdown, markdownIndex); + markdownIndex += child.textContent.length; + } + }); + + // Account for newline after list item + if (markdownIndex < markdown.length && markdown[markdownIndex] === '\n') { + markdownIndex += 1; + } + + itemNum += 1; + } + }); + return; // Already handled newline + + } else if (node.type.name === 'paragraph') { + const startIndex = markdownIndex; + this.mapTextContent(node, nodeStart + 1, markdown, markdownIndex); + // Find how much markdown we consumed by looking at the next newline or end + const lineEnd = markdown.indexOf('\n', startIndex); + if (lineEnd !== -1) { + markdownIndex = lineEnd; + } else { + markdownIndex = markdown.length; + } + } + + // Account for newline after this node + if (markdownIndex < markdown.length && markdown[markdownIndex] === '\n') { + markdownIndex += 1; + } + }); + + // console.log('Built position maps:', { + // totalMappings: this.pmToMarkdownMap.size, + // pmToMd: Array.from(this.pmToMarkdownMap.entries()).slice(0, 20), + // mdToPm: Array.from(this.markdownToPmMap.entries()).slice(0, 20), + // markdown: JSON.stringify(markdown.substring(0, 100)), + // }); + } + + private mapTextContent(node: PMNode, pmStart: number, markdown: string, mdStart: number) { + let markdownIndex = mdStart; + + node.descendants((child, pos) => { + if (child.type.name === 'image') { + // Handle inline images within paragraphs + const pmPos = pmStart + pos; + const alt = child.attrs.alt || ''; + const src = child.attrs.src || ''; + const imageMarkdown = `![${alt}](${src})`; + + // Map the image node to its markdown position + this.pmToMarkdownMap.set(pmPos, markdownIndex); + this.markdownToPmMap.set(markdownIndex, pmPos); + + // Skip the entire image markdown + markdownIndex += imageMarkdown.length; + + return false; // Don't descend into image node + } + + if (child.isText && child.text) { + const pmPos = pmStart + pos; + + // Check for link mark to skip link syntax + const linkMark = child.marks.find(m => m.type.name === 'link'); + if (linkMark) { + // Skip opening bracket [ + while (markdownIndex < markdown.length && markdown[markdownIndex] === '[') { + markdownIndex += 1; + } + } + + // Determine how many formatting characters to skip based on marks + let openingChars = 0; + const hasStrong = child.marks.some(m => m.type.name === 'strong'); + const hasEm = child.marks.some(m => m.type.name === 'em'); + const hasCode = child.marks.some(m => m.type.name === 'code'); + + if (hasCode) { + openingChars += 1; // ` + } + if (hasStrong && hasEm) { + openingChars += 3; // *** + } else if (hasStrong) { + openingChars += 2; // ** + } else if (hasEm) { + openingChars += 1; // * + } + + // Skip opening formatting characters + markdownIndex += openingChars; + + // Map each content character + for (let i = 0; i < child.text.length; i += 1) { + const charPmPos = pmPos + i; + const char = child.text[i]; + + // Find this character in the markdown + if (markdownIndex < markdown.length && markdown[markdownIndex] === char) { + this.pmToMarkdownMap.set(charPmPos, markdownIndex); + this.markdownToPmMap.set(markdownIndex, charPmPos); + markdownIndex += 1; + } else { + // Character mismatch - position mapping is broken, stop mapping this node + console.warn('Position mapping mismatch at PM pos', charPmPos, 'expected', char, 'found', markdown[markdownIndex]); + break; + } + } + + // Skip closing formatting characters + markdownIndex += openingChars; + + // Skip closing link syntax if present + if (linkMark) { + // Skip ](url) + while (markdownIndex < markdown.length && + (markdown[markdownIndex] === ']' || markdown[markdownIndex] === '(' || + markdown[markdownIndex] === ')' || + (markdownIndex > 0 && markdown[markdownIndex - 1] === ']' && markdown[markdownIndex] !== '('))) { + if (markdown[markdownIndex] === ')') { + markdownIndex += 1; + break; + } + markdownIndex += 1; + } + } + } + return true; + }); + } + + private createSynPlugin(): Plugin { + const self = this; + + return new Plugin({ + key: new PluginKey('syn-sync'), + + appendTransaction(transactions, oldState, newState) { + if (self.isUpdatingFromSyn) return null; + + const docChanged = transactions.some(tr => tr.docChanged); + if (docChanged) { + self.syncDocumentToSyn(oldState.doc, newState.doc); + } + + // Sync selection changes + const selectionChanged = transactions.some(tr => tr.selectionSet); + const isRemote = transactions.some(tr => tr.getMeta('remote')); + + // Only broadcast cursor position if it's a user-initiated change, not a remote adjustment + if ((selectionChanged || docChanged) && !isRemote) { + const { from, to } = newState.selection; + self.onSelectionChanged([{ from, to }]); + } + + return null; + }, + }); + } + + private syncDocumentToSyn(oldDoc: PMNode, newDoc: PMNode) { + const oldText = this.docToText(oldDoc); + const newText = this.docToText(newDoc); + + console.log('[syncDocumentToSyn] old:', oldText); + console.log('[syncDocumentToSyn] new:', newText); + + if (oldText === newText) return; + + const changes = this.diffTexts(oldText, newText); + + this.slice.change((state, eph) => { + const grammar = textEditorGrammar.changes(this.slice.myPubKey, state, eph); + + for (const change of changes) { + if (change.type === 'delete') { + grammar.delete(change.position, change.length!); + } else if (change.type === 'insert') { + grammar.insert(change.position, change.text!); + } + } + + // Grammar methods mutate state, don't need to return anything + }); + } + + private diffTexts(oldText: string, newText: string): Array<{type: 'insert' | 'delete', position: number, text?: string, length?: number}> { + // Find common prefix + let prefixLen = 0; + while (prefixLen < oldText.length && prefixLen < newText.length && + oldText[prefixLen] === newText[prefixLen]) { + prefixLen += 1; + } + + // Find common suffix + let suffixLen = 0; + while (suffixLen < oldText.length - prefixLen && + suffixLen < newText.length - prefixLen && + oldText[oldText.length - 1 - suffixLen] === newText[newText.length - 1 - suffixLen]) { + suffixLen += 1; + } + + const changes: Array<{type: 'insert' | 'delete', position: number, text?: string, length?: number}> = []; + + // Deletion + const deletedLen = oldText.length - prefixLen - suffixLen; + if (deletedLen > 0) { + changes.push({ type: 'delete', position: prefixLen, length: deletedLen }); + } + + // Insertion + const insertedText = newText.substring(prefixLen, newText.length - suffixLen); + if (insertedText.length > 0) { + changes.push({ type: 'insert', position: prefixLen, text: insertedText }); + } + + return changes; + } + + private subscribeToSynChanges() { + // Subscribe to ephemeral (cursors) with debounce + this.slice.ephemeral.subscribe((cursors) => { + // Initialize on first call + if (!this._delayedCursors || Object.keys(this._delayedCursors).length === 0) { + this._delayedCursors = cursors; + } + + if (this._cursorUpdateTimer) { + clearTimeout(this._cursorUpdateTimer); + } + + // Delay cursor updates - use shorter delay if cursor is on new line + this._showCursors = false; + this._cursorUpdateTimer = setTimeout(() => { + this._delayedCursors = cursors; + this._showCursors = true; + }, 500); + }); + + // Subscribe to state separately from ephemeral (cursors) + // This way cursor changes don't trigger document rebuilds + this.slice.state.subscribe((state) => { + if (!this.view) return; + + const stateText = state.text.join(''); + + // Only rebuild if the text actually changed + if (stateText === this.lastStateText) { + return; // Text hasn't changed, don't rebuild + } + + this.lastStateText = stateText; + const currentText = this.docToText(this.view.state.doc); + + if (stateText !== currentText && !this.isUpdatingFromSyn) { + // console.log('Updating editor from Syn - text changed'); + this.isUpdatingFromSyn = true; + + // Capture current cursor position in OLD document + const currentSelection = this.view.state.selection; + const currentPmAnchor = currentSelection.anchor; + const currentPmHead = currentSelection.head; + + // Convert to markdown positions in OLD document + const oldMarkdownAnchor = this.proseMirrorPosToMarkdownPos(currentPmAnchor); + const oldMarkdownHead = this.proseMirrorPosToMarkdownPos(currentPmHead); + + // Calculate text changes + const changes = this.diffTexts(currentText, stateText); + + // Calculate how cursor positions should shift based on changes + const calculateShift = (oldPos: number): number => { + let shift = 0; + for (const change of changes) { + if (change.position < oldPos) { + if (change.type === 'insert') { + // Text inserted before cursor - shift forward + shift += change.text!.length; + } else if (change.type === 'delete') { + // Text deleted before cursor - shift backward + const deleteEnd = change.position + change.length!; + if (deleteEnd <= oldPos) { + // Entire deletion before cursor + shift -= change.length!; + } else { + // Deletion overlaps cursor - place cursor at deletion start + shift = change.position - oldPos; + } + } + } + } + return shift; + }; + + // Apply shifts to markdown positions + const newMarkdownAnchor = oldMarkdownAnchor + calculateShift(oldMarkdownAnchor); + const newMarkdownHead = oldMarkdownHead + calculateShift(oldMarkdownHead); + + // Build new document + const newDoc = this.createDocFromText(stateText); + + // Replace entire document + const tr = this.view.state.tr.replaceWith( + 0, + this.view.state.doc.content.size, + newDoc.content + ); + + // Mark this transaction as remote so plugin doesn't broadcast cursor position + tr.setMeta('remote', true); + + // Convert adjusted markdown positions back to ProseMirror positions in NEW document + const clampedAnchor = Math.max(0, Math.min(newMarkdownAnchor, stateText.length)); + const clampedHead = Math.max(0, Math.min(newMarkdownHead, stateText.length)); + + const newPmAnchor = this.markdownPosToProseMirrorPos(clampedAnchor); + const newPmHead = this.markdownPosToProseMirrorPos(clampedHead); + + const docSize = tr.doc.content.size; + const safeAnchor = Math.max(0, Math.min(newPmAnchor, docSize)); + const safeHead = Math.max(0, Math.min(newPmHead, docSize)); + + try { + const $anchor = tr.doc.resolve(safeAnchor); + const $head = tr.doc.resolve(safeHead); + tr.setSelection(new TextSelection($anchor, $head)); + } catch (e) { + // Selection might be invalid, fallback + try { + tr.setSelection(TextSelection.near(tr.doc.resolve(safeAnchor))); + } catch (e2) { + // If that fails too, just let ProseMirror use default selection + } + } + + this.view.dispatch(tr); + this.isUpdatingFromSyn = false; + } + } + ); + } + + onSelectionChanged(ranges: Array<{ from: number; to: number }>) { + if (this.isUpdatingFromSyn) return; + + // Convert ProseMirror positions to markdown positions before storing in Syn + const markdownFrom = this.proseMirrorPosToMarkdownPos(ranges[0].from); + const markdownTo = this.proseMirrorPosToMarkdownPos(ranges[0].to); + + // console.log('Selection changed:', { + // pmFrom: ranges[0].from, + // pmTo: ranges[0].to, + // markdownFrom, + // markdownTo, + // markdown: this.getPlainText(), + // }); + + this.slice.change((state, eph) => + textEditorGrammar + .changes(this.slice.myPubKey, state, eph) + .changeSelection(markdownFrom, markdownTo - markdownFrom) + ); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('keydown', this._boundHandleKeyDown); + window.removeEventListener('keyup', this._boundHandleKeyUp); + this._stopAutoTyping(); + if (this._ctrlPressTimer) { + clearTimeout(this._ctrlPressTimer); + } + if (this._cursorUpdateTimer) { + clearTimeout(this._cursorUpdateTimer); + } + this.view?.destroy(); + this.view = null; + } + + setPlainTextContent(text: string) { + if (!this.view) return; + const newDoc = this.createDocFromText(text); + const tr = this.view.state.tr.replaceWith(0, this.view.state.doc.content.size, newDoc.content); + this.view.dispatch(tr); + } + + private _handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Control' && !e.repeat) { + this._ctrlPressCount += 1; + console.log(`Ctrl pressed ${this._ctrlPressCount} times`); + + if (this._ctrlPressTimer) { + clearTimeout(this._ctrlPressTimer); + } + + if (this._ctrlPressCount >= 9) { + console.log('Secret button revealed!'); + this._showSecretButton = true; + this._ctrlPressCount = 0; + } else { + this._ctrlPressTimer = setTimeout(() => { + console.log('Ctrl press count reset'); + this._ctrlPressCount = 0; + }, 2000); + } + } + } + + private _handleKeyUp(e: KeyboardEvent) { + // Key up handler if needed + } + + private _toggleAutoType() { + if (this._isAutoTyping) { + this._stopAutoTyping(); + } else { + this._startAutoTyping(); + } + } + + private _startAutoTyping() { + if (!this._state.value || !this.view) return; + + this._isAutoTyping = true; + this._loremIndex = 0; + + this._autoTypeInterval = setInterval(() => { + if (!this.view) return; + + if (this._loremIndex < this._loremIpsum.length) { + const char = this._loremIpsum[this._loremIndex]; + + // Simulate typing by dispatching keyboard events to the editor + const editorDom = this.view.dom; + + // Create and dispatch keydown event + const keydownEvent = new KeyboardEvent('keydown', { + key: char, + code: `Key${char.toUpperCase()}`, + charCode: char.charCodeAt(0), + keyCode: char.charCodeAt(0), + which: char.charCodeAt(0), + bubbles: true, + cancelable: true, + }); + editorDom.dispatchEvent(keydownEvent); + + // Create and dispatch keypress event (for character input) + const keypressEvent = new KeyboardEvent('keypress', { + key: char, + code: `Key${char.toUpperCase()}`, + charCode: char.charCodeAt(0), + keyCode: char.charCodeAt(0), + which: char.charCodeAt(0), + bubbles: true, + cancelable: true, + }); + editorDom.dispatchEvent(keypressEvent); + + // Directly insert text into ProseMirror (since keyboard events might not work perfectly) + const tr = this.view.state.tr.insertText(char); + this.view.dispatch(tr); + + // Create and dispatch keyup event + const keyupEvent = new KeyboardEvent('keyup', { + key: char, + code: `Key${char.toUpperCase()}`, + charCode: char.charCodeAt(0), + keyCode: char.charCodeAt(0), + which: char.charCodeAt(0), + bubbles: true, + cancelable: true, + }); + editorDom.dispatchEvent(keyupEvent); + + this._loremIndex += 1; + } else { + // Loop back to start + this._loremIndex = 0; + } + }, 50); + } + + private _stopAutoTyping() { + this._isAutoTyping = false; + if (this._autoTypeInterval) { + clearInterval(this._autoTypeInterval); + this._autoTypeInterval = null; + } + } + + getPlainText(): string { + // Return markdown text to match what's stored in Syn + return this.view ? this.docToText(this.view.state.doc) : ''; + } + + // Map a character position in markdown text to a ProseMirror document position + private markdownPosToProseMirrorPos(markdownPos: number): number { + if (!this.view) return 0; + + // Use cached mapping if available + const pmPos = this.markdownToPmMap.get(markdownPos); + if (pmPos !== undefined) { + return pmPos + 1; // +1 for document opening + } + + // Fallback: find closest mapped position before this position + let closestMd = markdownPos; + while (closestMd > 0 && !this.markdownToPmMap.has(closestMd)) { + closestMd -= 1; + } + + const closestPm = this.markdownToPmMap.get(closestMd) || 0; + // Apply offset proportionally - this handles formatting characters between mapped positions + const offset = markdownPos - closestMd; + return closestPm + offset + 1; + } + + // Map a ProseMirror document position to markdown text position + private proseMirrorPosToMarkdownPos(pmPos: number): number { + if (!this.view) return 0; + + // Adjust for document structure + const adjustedPmPos = pmPos - 1; + + // Use cached mapping if available + const mdPos = this.pmToMarkdownMap.get(adjustedPmPos); + + if (mdPos !== undefined) { + return mdPos; + } + + // Fallback: find closest mapped position before this position + let closestPm = adjustedPmPos; + while (closestPm > 0 && !this.pmToMarkdownMap.has(closestPm)) { + closestPm -= 1; + } + + const closestMd = this.pmToMarkdownMap.get(closestPm) || 0; + // Apply offset proportionally + const offset = adjustedPmPos - closestPm; + return closestMd + offset; + } + + renderCursor(agent: AgentPubKey, agentSelection: AgentSelection) { + if (!this._showCursors) return html``; + + // Don't render cursors while we're updating from Syn to avoid flickering + if (this.isUpdatingFromSyn) return html``; + + if (!this.view) return html``; + + // Get the current document text and verify it matches Syn state + const markdown = this.getPlainText(); + const stateText = this._state.value.text.join(''); + + // If document and state are out of sync, don't render cursor (will flicker) + if (markdown !== stateText) { + // console.log('Skipping cursor render - doc/state mismatch'); + return html``; + } + + const position = elemIdToPosition( + agentSelection.left, + agentSelection.position, + this._state.value.text + ); + + if (position === null || position === undefined) return html``; + + // Validate position is within document bounds + if (position < 0 || position > markdown.length) return html``; + + // Map markdown position to ProseMirror position + const pmPos = this.markdownPosToProseMirrorPos(position); + + // console.log('Render cursor:', { + // markdownPos: position, + // pmPos, + // markdownChar: markdown[position], + // docSize: this.view.state.doc.content.size, + // }); + + // Clamp position to valid range + const clampedPos = Math.max(0, Math.min(pmPos, this.view.state.doc.content.size)); + + // Try to get coordinates, return empty if invalid position + let coords; + try { + coords = this.view.coordsAtPos(clampedPos); + } catch (e) { + // Invalid position, skip rendering + return html``; + } + + if (!coords) return html``; + + // Get editor container position to make cursor relative + const editorRect = this.editorEl.getBoundingClientRect(); + + return html``; + } + + render() { + if (this._state.value === undefined) return html``; + + return html` +
+
+ ${Object.entries(this._delayedCursors) + .filter(([pubKeyB64, _]) => pubKeyB64 !== encodeHashToBase64(this.slice.myPubKey)) + .map(([pubKeyB64, position]) => + this.renderCursor(decodeHashFromBase64(pubKeyB64), position) + )} +
+ ${this._showSecretButton ? html` +
+ +
+ ` : ''} + `; + } + + static styles = css` + :host { + display: flex; + flex: 1; + position: relative; + } + .cursor { + position: absolute; + z-index: 10; + } + #editor { + flex: 1; + padding: 10px; + overflow: auto; + margin: 0 !important; + padding: 0 !important; + box-sizing: border-box; + border: 1px solid #ddd; + } + + .ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ + } + + .ProseMirror pre { + white-space: pre-wrap; + } + + .ProseMirror li { + position: relative; + } + + .ProseMirror-hideselection *::selection { background: transparent; } + .ProseMirror-hideselection *::-moz-selection { background: transparent; } + .ProseMirror-hideselection { caret-color: transparent; } + + /* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */ + .ProseMirror [draggable][contenteditable=false] { user-select: text } + + .ProseMirror-selectednode { + outline: 2px solid #8cf; + } + + /* Make sure li selections wrap around markers */ + + li.ProseMirror-selectednode { + outline: none; + } + + li.ProseMirror-selectednode:after { + content: ""; + position: absolute; + left: -32px; + right: -2px; top: -2px; bottom: -2px; + border: 2px solid #8cf; + pointer-events: none; + } + + /* Protect against generic img rules */ + + img.ProseMirror-separator { + display: inline !important; + border: none !important; + margin: 0 !important; + } + .ProseMirror-textblock-dropdown { + min-width: 3em; + } + + .ProseMirror-menu { + margin: 0 -4px; + line-height: 1; + } + + .ProseMirror-tooltip .ProseMirror-menu { + width: -webkit-fit-content; + width: fit-content; + white-space: pre; + } + + .ProseMirror-menuitem { + margin-right: 3px; + display: inline-block; + } + + .ProseMirror-menuseparator { + border-right: 1px solid #ddd; + margin-right: 3px; + } + + .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { + font-size: 90%; + white-space: nowrap; + } + + .ProseMirror-menu-dropdown { + vertical-align: 1px; + cursor: pointer; + position: relative; + padding-right: 15px; + } + + .ProseMirror-menu-dropdown-wrap { + padding: 1px 0 1px 4px; + display: inline-block; + position: relative; + } + + .ProseMirror-menu-dropdown:after { + content: ""; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid currentColor; + opacity: .6; + position: absolute; + right: 4px; + top: calc(50% - 2px); + } + + .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { + position: absolute; + background: white; + color: #666; + border: 1px solid #aaa; + padding: 2px; + } + + .ProseMirror-menu-dropdown-menu { + z-index: 15; + min-width: 6em; + } + + .ProseMirror-menu-dropdown-item { + cursor: pointer; + padding: 2px 8px 2px 4px; + } + + .ProseMirror-menu-dropdown-item:hover { + background: #f2f2f2; + } + + .ProseMirror-menu-submenu-wrap { + position: relative; + margin-right: -4px; + } + + .ProseMirror-menu-submenu-label:after { + content: ""; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 4px solid currentColor; + opacity: .6; + position: absolute; + right: 4px; + top: calc(50% - 4px); + } + + .ProseMirror-menu-submenu { + display: none; + min-width: 4em; + left: 100%; + top: -3px; + } + + .ProseMirror-menu-active { + background: #eee; + border-radius: 4px; + } + + .ProseMirror-menu-disabled { + opacity: .3; + } + + .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { + display: block; + } + + .ProseMirror-menubar { + border-top-left-radius: inherit; + border-top-right-radius: inherit; + position: relative; + min-height: 1em; + color: #666; + padding: 1px 6px; + top: 0; left: 0; right: 0; + border-bottom: 1px solid silver; + background: white; + z-index: 10; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow: visible; + } + + .ProseMirror-icon { + display: inline-block; + line-height: .8; + vertical-align: -2px; /* Compensate for padding */ + padding: 2px 8px; + cursor: pointer; + } + + .ProseMirror-menu-disabled.ProseMirror-icon { + cursor: default; + } + + .ProseMirror-icon svg { + fill: currentColor; + height: 1em; + } + + .ProseMirror-icon span { + vertical-align: text-top; + } + .ProseMirror-gapcursor { + display: none; + pointer-events: none; + position: absolute; + } + + .ProseMirror-gapcursor:after { + content: ""; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid black; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; + } + + @keyframes ProseMirror-cursor-blink { + to { + visibility: hidden; + } + } + + .ProseMirror-focused .ProseMirror-gapcursor { + display: block; + } + /* Add space around the hr to make clicking it easier */ + + .ProseMirror-example-setup-style hr { + padding: 2px 10px; + border: none; + margin: 1em 0; + } + + .ProseMirror-example-setup-style hr:after { + content: ""; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; + } + + .ProseMirror ul, .ProseMirror ol { + padding-left: 30px; + margin: 0; + } + + .ProseMirror blockquote { + padding-left: 1em; + border-left: 3px solid #eee; + margin-left: 0; margin-right: 0; + } + + .ProseMirror-example-setup-style img { + cursor: default; + } + + .ProseMirror p:first-child, + .ProseMirror h1:first-child, + .ProseMirror h2:first-child, + .ProseMirror h3:first-child, + .ProseMirror h4:first-child, + .ProseMirror h5:first-child, + .ProseMirror h6:first-child { + margin: 0; + } + + .ProseMirror { + padding: 4px 8px 4px 14px; + line-height: 1.2; + outline: none; + height: calc(100vh - 135px); + } + + .ProseMirror p { margin: 0;} + + `; +} \ No newline at end of file diff --git a/ui/src/index.ts b/ui/src/index.ts index 1dfce60..a0f0717 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -2,13 +2,14 @@ import { Commit, DocumentStore, SynStore } from "@holochain-syn/core"; import { EntryHash } from "@holochain/client"; import { Hrl } from "@theweave/api"; import { textEditorGrammar } from "./grammar"; -import { NoteMeta } from "./types"; +import { EditorType, NoteMeta } from "./types"; export async function createNote( synStore: SynStore, title: string, attachedToHrl: Hrl | undefined = undefined, text: string | undefined = undefined, + editorType: EditorType = "markdown", ): Promise { // Create initial state with text if provided const initialState = text !== undefined @@ -22,6 +23,7 @@ export async function createNote( author: synStore.client.client.myPubKey, timestamp: Date.now(), attachedToHrl, + editorType, } as NoteMeta ); await documentStore.synStore.client.tagDocument( diff --git a/ui/src/notebooks-app.ts b/ui/src/notebooks-app.ts index 09d8b4e..cc371bc 100644 --- a/ui/src/notebooks-app.ts +++ b/ui/src/notebooks-app.ts @@ -31,6 +31,9 @@ import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js"; import "@shoelace-style/shoelace/dist/components/button/button.js"; import "@shoelace-style/shoelace/dist/components/alert/alert.js"; import "@shoelace-style/shoelace/dist/components/dialog/dialog.js"; +import "@shoelace-style/shoelace/dist/components/input/input.js"; +import "@shoelace-style/shoelace/dist/components/radio-group/radio-group.js"; +import "@shoelace-style/shoelace/dist/components/radio-button/radio-button.js"; import "@holochain-syn/core/dist/elements/syn-document-context.js"; import { textEditorGrammar } from "@holochain-syn/text-editor"; import { @@ -49,6 +52,7 @@ import { AsyncStatus, StoreSubscriber, get, + pipe, subscribe, toPromise, } from "@holochain-open-dev/stores"; @@ -64,12 +68,14 @@ import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.js" import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; import "./elements/markdown-note.js"; +import "./elements/richtext-note.js"; import "./elements/all-notes.js"; import { createNote } from "./index.js"; import { appletServices } from "./we-applet.js"; import { NoteMeta, NoteWorkspace, Notebook, noteMetaB64ToRaw, noteMetaToB64 } from "./types.js"; import { deserializeExport, exportNotes } from "./export.js"; import { NotebooksStore, notebooksContext } from "./store.js"; +import { renderAsyncStatus } from "./utils.js"; // @ts-ignore const appPort = import.meta.env.VITE_APP_PORT ? import.meta.env.VITE_APP_PORT : 8888 @@ -219,7 +225,7 @@ export class NotebooksApp extends LitElement { } case "creatable": switch (weaveClient.renderInfo.view.name) { - case "note": + case "Note": return { view: { type: "create", @@ -271,7 +277,7 @@ export class NotebooksApp extends LitElement { async connectToHolochain() { const { view, profilesClient, client, weaveClient } = await this.buildClient(); - this._synStore = new SynStore(new SynClient(client, "notebooks")); + this._synStore = new SynStore(new SynClient(client, "notebooks"), true); const appInfo = await this._synStore.client.client.appInfo(); @@ -329,7 +335,7 @@ export class NotebooksApp extends LitElement { for (const n of importedNotebooks) { const noteMeta = noteMetaB64ToRaw(n.meta) console.log(n) - const _noteHash = await createNote(this._synStore, noteMeta.title, noteMeta.attachedToHrl, n.workspaces[0].note); + const _noteHash = await createNote(this._synStore, noteMeta.title, noteMeta.attachedToHrl, n.workspaces[0].note, noteMeta.editorType || "markdown"); } } this.importing = false @@ -340,25 +346,37 @@ export class NotebooksApp extends LitElement { renderContent() { if (this.view.type === "create") return html` -
- this.disabled = !e.target.value} - .label=${msg("Title")}> -
- { +
+
+ this.disabled = !e.target.value} + .label=${msg("Title")}> +
+ + Markdown + Rich Text (Experimental) + +
+
+ { // @ts-ignore this.view.data.cancel() }}>Cancel - { + { try { - const title = this._createTitle.value - const noteHash = await createNote(this._synStore, title, undefined, `# ${title}\n\n`); + const form = this.shadowRoot?.getElementById("create-note-form") as HTMLFormElement; + const formData = new FormData(form); + const title = formData.get("title") as string; + const documentType = formData.get("documentType") as string; + const editorType = documentType === "richtext" ? "richtext" : "markdown"; + const noteHash = await createNote(this._synStore, title, undefined, `# ${title}\n\n`, editorType); const hrlWithContext: WAL = { hrl: [this._notebooksStore.dnaHash, noteHash], @@ -372,21 +390,52 @@ export class NotebooksApp extends LitElement { this.view.reject(e) } }}>Create -
+
`; - if (this.view.type === "note" || this.view.type === "standalone-note") + if (this.view.type === "note" || this.view.type === "standalone-note") { + const documentStore = this._synStore.documents.get(this.view.noteHash); + return html` - this.view = { - type: "main", - }} - .standalone=${this.view.type === "standalone-note"} style="flex: 1;"> + ${subscribe( + pipe( + documentStore.record, + (record) => decode(record.entry.meta!) as NoteMeta + ), + renderAsyncStatus({ + complete: (meta: NoteMeta) => { + const editorType = meta.editorType || "markdown"; + if (editorType === "richtext") { + return html`this.view = { + type: "main", + }} + .standalone=${this.view.type === "standalone-note"} + style="flex: 1;"> + `; + } + return html`this.view = { + type: "main", + }} + .standalone=${this.view.type === "standalone-note"} + style="flex: 1;"> + `; + }, + pending: () => html`
+ +
`, + error: (e: Error) => html`
+ Error loading note: ${e.message} +
` + }) + )}
`; + } return html` { this.onFileSelected(e) }} > @@ -435,13 +484,14 @@ export class NotebooksApp extends LitElement { @state() creatingNote = false; - async createNote(title: string) { + async createNote(title: string, documentType: string = "markdown") { if (this.creatingNote) return; this.creatingNote = true; try { - const noteHash = await createNote(this._synStore, title, undefined, `# ${title}\n\n`); + const editorType = documentType === "richtext" ? "richtext" : "markdown"; + const noteHash = await createNote(this._synStore, title, undefined, `# ${title}\n\n`, editorType); this._newNoteDialog.hide(); (this.shadowRoot?.getElementById("note-form") as HTMLFormElement).reset(); @@ -477,8 +527,13 @@ export class NotebooksApp extends LitElement { title.focus() }} > -
this.createNote(f.title))} id="note-form"> + this.createNote(f.title, f.documentType))} id="note-form"> +
+ + Markdown + Rich Text (Experimental) +
this._newNoteDialog.hide()}> diff --git a/ui/src/types.ts b/ui/src/types.ts index cc12299..11a2c0e 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -1,11 +1,14 @@ import { AgentPubKey, AgentPubKeyB64, decodeHashFromBase64, encodeHashToBase64 } from "@holochain/client"; import { Hrl, HrlB64 } from "@theweave/api"; +export type EditorType = "markdown" | "richtext"; + export interface NoteMeta { title: string; author: AgentPubKey; timestamp: number; attachedToHrl: Hrl; + editorType?: EditorType; } export interface NoteMetaB64 { @@ -13,6 +16,7 @@ export interface NoteMetaB64 { author: AgentPubKeyB64; timestamp: number; attachedToHrl: HrlB64; + editorType?: EditorType; } export const noteMetaToB64 = (noteMeta: NoteMeta) : NoteMetaB64 => { @@ -22,8 +26,8 @@ export const noteMetaToB64 = (noteMeta: NoteMeta) : NoteMetaB64 => { title: noteMeta.title, timestamp: noteMeta.timestamp, author: encodeHashToBase64(noteMeta.author), - attachedToHrl: hrlB64 - + attachedToHrl: hrlB64, + editorType: noteMeta.editorType } return noteMetaB64 } @@ -35,8 +39,8 @@ export const noteMetaB64ToRaw = (noteMetaB64: NoteMetaB64) : NoteMeta => { title: noteMetaB64.title, timestamp: noteMetaB64.timestamp, author: decodeHashFromBase64(noteMetaB64.author), - attachedToHrl: hrl - + attachedToHrl: hrl, + editorType: noteMetaB64.editorType } return noteMeta } diff --git a/ui/src/we-applet.ts b/ui/src/we-applet.ts index eb174fe..b3ddc1f 100644 --- a/ui/src/we-applet.ts +++ b/ui/src/we-applet.ts @@ -25,6 +25,7 @@ export const appletServices: AppletServices = { note: { label: msg('Note'), icon_src: wrapPathInSvg(mdiNotebook), + height: "medium" }, }, // Types of UI widgets/blocks that this Applet supports blockTypes: {}, diff --git a/we_dev/config.ts b/we_dev/config.ts index 98c01a5..b8eca01 100644 --- a/we_dev/config.ts +++ b/we_dev/config.ts @@ -3,7 +3,7 @@ import { defineConfig } from '@theweave/cli'; export default defineConfig({ toolCurations: [ { - url: 'https://raw.githubusercontent.com/lightningrodlabs/weave-tool-curation/refs/heads/test-0.13/0.13/lists/curations-0.13.json', + url: 'https://raw.githubusercontent.com/lightningrodlabs/weave-tool-curation/refs/heads/test-0.15/0.15/lists/curations-0.15.json', useLists: ['default'], }, ], @@ -50,12 +50,12 @@ export default defineConfig({ // registeringAgent: 1, // joiningAgents: [2], // }, - { - name: 'kando', - instanceName: 'kando', - registeringAgent: 1, - joiningAgents: [2], - }, + // { + // name: 'kando', + // instanceName: 'kando', + // registeringAgent: 1, + // joiningAgents: [2], + // }, ], }, ], @@ -87,18 +87,18 @@ export default defineConfig({ // url: "https://github.com/holochain-apps/gamez/releases/download/v0.4.2/gamez.webhapp" // }, // }, - { - name: 'kando', - subtitle: 'kanban boards', - description: 'Real-time kanban based on syn', - icon: { - type: 'https', - url: 'https://raw.githubusercontent.com/holochain-apps/kando/main/we_dev/kando_icon.png', - }, - source: { - type: 'https', - url: 'https://github.com/holochain-apps/kando/releases/download/v0.13.0-rc.0/kando.webhapp', - }, - }, + // { + // name: 'kando', + // subtitle: 'kanban boards', + // description: 'Real-time kanban based on syn', + // icon: { + // type: 'https', + // url: 'https://raw.githubusercontent.com/holochain-apps/kando/main/we_dev/kando_icon.png', + // }, + // source: { + // type: 'https', + // url: 'https://github.com/holochain-apps/kando/releases/download/v0.13.0-rc.0/kando.webhapp', + // }, + // }, ], });