Compare commits

...

26 Commits

Author SHA1 Message Date
9df4a09414 1.9.7
Some checks failed
Default (tags) / security (push) Failing after 35s
Default (tags) / test (push) Failing after 29s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 19:19:19 +00:00
7cbc941407 update readme 2025-06-26 19:18:58 +00:00
b31f306106 1.9.6
Some checks failed
Default (tags) / security (push) Failing after 36s
Default (tags) / test (push) Failing after 29s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 19:00:57 +00:00
1dbbac450c update dees-tags 2025-06-26 19:00:15 +00:00
b5a2bd7436 fix zindex 2025-06-26 18:37:49 +00:00
931a760ee1 update z-index showcase 2025-06-26 18:13:08 +00:00
27414e0284 1.9.5
Some checks failed
Default (tags) / security (push) Failing after 39s
Default (tags) / test (push) Failing after 31s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:57:43 +00:00
d63bc762d0 update 2025-06-26 15:57:27 +00:00
505e40a57f update modals 2025-06-26 15:51:05 +00:00
d1ea10d8c6 update z-index use 2025-06-26 15:46:44 +00:00
1038759d8b 1.9.4
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 28s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:33:18 +00:00
ab9b545c9a update file upload 2025-06-26 15:32:29 +00:00
e1329ecd7a 1.9.3
Some checks failed
Default (tags) / security (push) Failing after 43s
Default (tags) / test (push) Failing after 24s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:08:25 +00:00
167df241b7 update 2025-06-26 15:08:14 +00:00
b41e9f31e7 add proper input demo page 2025-06-26 14:46:37 +00:00
02f25aa02e fix(editor-demos): update 2025-06-26 14:27:39 +00:00
312fc4ba90 feat(dees-input-richtext): use lucide icons 2025-06-26 14:15:52 +00:00
56d7b44b01 feat(prosemirror): add prosemirror support 2025-06-26 14:12:06 +00:00
f72c9fad3a update navigation 2025-06-26 13:45:00 +00:00
d48fd667a2 update 2025-06-26 13:38:09 +00:00
979877b3b0 update 2025-06-26 13:32:37 +00:00
342bd7d7c2 update 2025-06-26 13:18:34 +00:00
4d42911198 update 2025-06-26 12:00:35 +00:00
3ea7186d6c update 2025-06-26 11:57:04 +00:00
09e35d0245 update codeblock 2025-06-26 11:41:58 +00:00
4a26307e1b update 2025-06-25 05:30:20 +00:00
56 changed files with 8989 additions and 2886 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
# artifacts # artifacts
coverage/ coverage/
public/ public/
pages/
# installs # installs
node_modules/ node_modules/

View File

@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.9.2", "version": "1.9.7",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@ -25,6 +25,12 @@
"@push.rocks/smarti18n": "^1.0.4", "@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0", "@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@tiptap/core": "^2.22.3",
"@tiptap/extension-link": "^2.22.3",
"@tiptap/extension-text-align": "^2.22.3",
"@tiptap/extension-typography": "^2.22.3",
"@tiptap/extension-underline": "^2.22.3",
"@tiptap/starter-kit": "^2.22.3",
"@tsclass/tsclass": "^9.2.0", "@tsclass/tsclass": "^9.2.0",
"@webcontainer/api": "1.2.0", "@webcontainer/api": "1.2.0",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",

543
pnpm-lock.yaml generated
View File

@ -38,6 +38,24 @@ importers:
'@push.rocks/smartstring': '@push.rocks/smartstring':
specifier: ^4.0.15 specifier: ^4.0.15
version: 4.0.15 version: 4.0.15
'@tiptap/core':
specifier: ^2.22.3
version: 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-link':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-text-align':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-typography':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-underline':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/starter-kit':
specifier: ^2.22.3
version: 2.22.3
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.2.0 specifier: ^9.2.0
version: 9.2.0 version: 9.2.0
@ -1217,6 +1235,9 @@ packages:
resolution: {integrity: sha512-6KGnf2vHR7hW4mQpAD7gkDVL3QVML3jb/No/Uw+qCqvs0TaQr60Yjm+CXoLxJNCKwmrL+I1yx8mhAHBHfYJiJA==} resolution: {integrity: sha512-6KGnf2vHR7hW4mQpAD7gkDVL3QVML3jb/No/Uw+qCqvs0TaQr60Yjm+CXoLxJNCKwmrL+I1yx8mhAHBHfYJiJA==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarturl deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarturl
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rolldown/binding-darwin-arm64@1.0.0-beta.18': '@rolldown/binding-darwin-arm64@1.0.0-beta.18':
resolution: {integrity: sha512-F1kqKxIuh9XM6ViC3/Ltz6ARpyUo6b1b2Lo1BhMwR5KwQ06OdOAOY9fmVW5XJ9hHYzABGgvH4hfjtYad0KshAA==} resolution: {integrity: sha512-F1kqKxIuh9XM6ViC3/Ltz6ARpyUo6b1b2Lo1BhMwR5KwQ06OdOAOY9fmVW5XJ9hHYzABGgvH4hfjtYad0KshAA==}
cpu: [arm64] cpu: [arm64]
@ -1601,6 +1622,138 @@ packages:
'@tempfix/watcher@2.3.0': '@tempfix/watcher@2.3.0':
resolution: {integrity: sha512-a2qVQffcrnetehvwsN+LdipxQ6jejwZLgAvS9/91+C0gP4CKyikY01c0tSs0I4tSL7qHdCw1Fx0quLw+A9uyLA==} resolution: {integrity: sha512-a2qVQffcrnetehvwsN+LdipxQ6jejwZLgAvS9/91+C0gP4CKyikY01c0tSs0I4tSL7qHdCw1Fx0quLw+A9uyLA==}
'@tiptap/core@2.22.3':
resolution: {integrity: sha512-czyBPXZG/ZFyObZEF1kyusGf58Ai3X8TnaxlUUn3gqLLWPy0idXZg85NETCidzi/gAxWxL9j6Pcy+zwS4pbZYQ==}
peerDependencies:
'@tiptap/pm': ^2.7.0
'@tiptap/extension-blockquote@2.22.3':
resolution: {integrity: sha512-HvTXvqeGaANg0owk0Xxkgyc4lJMO5CZES2Lc3JJp8u5kV+HZIwd78eJ7fbKBMtkpKb4zOk4xQsHQ/TuhghJaeA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bold@2.22.3':
resolution: {integrity: sha512-J3GxKwijD42eqCwU1SS7PK5aSgnp0wgQDetLz9izAD0RQBrKj5WZA13GnPoTTlzLU4qwjcPRV+6mvF+llH6b6A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bullet-list@2.22.3':
resolution: {integrity: sha512-SYvLIxqmuV0kTj4/3ZFlnZ1fr9Y233qX00BKuIpGnczeFsWQmzBJo8vGm3d1IlKPCQN+jTRtDdDE1aSum8Kv2w==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-code-block@2.22.3':
resolution: {integrity: sha512-twPCBpb/ygNixlSBAXgvfo+t56Ucpb8lvPDiZn+cH8OjmmO0ayBoSfSrjKWgaEWGPcXBrFAfsBRbYHyoHj7pXg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.22.3':
resolution: {integrity: sha512-s+W6jHezq+n9cC40xZ3hZF6cGGSl+fBELik1b2x8+cb0WoIlqmcdWin1dgeMNrWlRZUw1aD2DNwy/PdXI5vn2g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-document@2.22.3':
resolution: {integrity: sha512-7MnILbhRZRyROlMUgyntzRZ/EZlqNB8fO761RNjJxR2WMb49R4yc04fz7/+f/QH/hwxoS13bKfsNUDAsDxA5Aw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.22.3':
resolution: {integrity: sha512-yQxSfTWjdUQS+bh6KiNLR9KIMsn1SElzycQe4XE+0eoaetapGtKqxfwkTbbQdNgQOU5wQG1KOda221mnPvkpAA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.22.3':
resolution: {integrity: sha512-6Q8TLL4PVGcZLn27eQazCC+be8LP8uzuz5Z5e4TpIeswPAju49cerQOdEGNFKkuYv/FelWIhXNtkWFMf4eSmyw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.22.3':
resolution: {integrity: sha512-tbEji/V4Za3UhxYwB36amYhyonwe5j66iYTNRWzgjNixjrcGDbWk6cfaF9jMAgPgIDBmmtQLJY+moKskwgpnZg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-heading@2.22.3':
resolution: {integrity: sha512-+MexJD+kXtNwMDbNTFa7jCFipx1DqAdT+n9GgInqebAN9bK+CWjC+SskzZNRqeMrQ0Er7QTsi6YC09M+74sevA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.22.3':
resolution: {integrity: sha512-F9sC45zPw7vbjKrwSKuSLZ0ODyc/X3bGPeCa6HYLEHKfgqsdt2v2fQLvxjpmlwO2ZMrnkBkg76KDxHfVyrZ2zQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-horizontal-rule@2.22.3':
resolution: {integrity: sha512-3GvY798p9pCXUBbCebIdSmi1q80l7VZz/B6NN4uUMQ9iwxWopd8yaZ0O7xx2hM2UBzPEtY3M4FAhhpYUTXNFgQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-italic@2.22.3':
resolution: {integrity: sha512-W/rQDo7qFL7MfwfaYEcdtbk862fOmBv30qIEwVdqElBye7BFJYKtRuWBzNbG2BwKanjwMbVc/tBXF5W1sqfT7Q==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.22.3':
resolution: {integrity: sha512-05whzrw8uuRHpFah27a+K3XVjZ78aifO1C5ncQiEgjG8oMbaPG0FpU7yJ9awNtr1U5dvFUEbGvkg1WDW2fLTzA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-list-item@2.22.3':
resolution: {integrity: sha512-B7Fze+eM1sYbGOZtDDAwAivnj1ow2wN5RqaQPC1la3wdTK4Wgp7bdzGjvUbrN6gp3zMFCEWlqP2toc/mRAHCtA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-ordered-list@2.22.3':
resolution: {integrity: sha512-pHGkuZhV/uAAHI9vzk/lpAkbdpMT4wUR1FI17/GE3zNrogfzx0VopCQrXq4+sQVsLUW4I6Cj6VeBjm9wB6qlIw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-paragraph@2.22.3':
resolution: {integrity: sha512-TYvgS7CweNFo/xVxsKWSt0wnm46Y8OtsfDSjnLbSC4Pj4ZNa6PU3zpvDTW+UxYakr+8zIPvI2WgLBkyTHq6oQA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-strike@2.22.3':
resolution: {integrity: sha512-I+s2Csw2cTHae2vFJiojnHK+NnQjDr6441mSlAd+e7kEly1kjZ4g7J+JMj02ajNQhr/ob8/hb5r6EdIyv2xtoA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-align@2.22.3':
resolution: {integrity: sha512-UZ8803BOrHLHSNFfooqgkm2AQsaK/7eE1deQGSazxft89KksAv1kZkEKFypOE8yw85Bg2NHH2Lp6n4tyz2n6/g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-style@2.22.3':
resolution: {integrity: sha512-M3FLOUPcO8fR+rM97mR2gQ54KFkdlAUQtEPKQpO1f312gtcVdBNxgq0WgqTnBY7thWLyqQSKiAsL6y88+JddSA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text@2.22.3':
resolution: {integrity: sha512-07cymWkPTfq6nuum88Yf90YYArbowed8nNiu0Tw3jCvwpzf9J9TDaovT+LAKuSKtrOsnNpFB/9IqUwFxZepOGw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-typography@2.22.3':
resolution: {integrity: sha512-pkZUMSDnt1vAR8XwoO9um7WDzBkclSYvWEBdqUN9pnH7Fpz2kbuer/Hqyk94vvWz2D/svndrg1Fb2dLEvfQopA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-underline@2.22.3':
resolution: {integrity: sha512-floLjh1UbQ2pKgdwfw7qCAJ5VojvH1uqj7xW2RCv79aWYUuJCPD6UBpaBOt/jv7gXDJJ9EeV3m2Hga49CXBrEQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm@2.22.3':
resolution: {integrity: sha512-uWPeIScnpQVCYdTnL140XgcvbT1qH288CstMJ6S0Y11lC5PclPK9CxfAipsqgWWrIK7yatxKUVCg6TzfG9zpmA==}
'@tiptap/starter-kit@2.22.3':
resolution: {integrity: sha512-GkvheaR2ORnHJ9g9R6xIT38w2uppGja/iAIrXLZ9vY1QuR+0cya/ZZ5vKU6r9C2PeyBs3aKYxRD1/j3HDhuGXw==}
'@tokenizer/token@0.3.0': '@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
@ -1755,9 +1908,18 @@ packages:
'@types/koa@2.15.0': '@types/koa@2.15.0':
resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/mime-types@2.1.4': '@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
@ -2300,6 +2462,9 @@ packages:
typescript: typescript:
optional: true optional: true
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
croner@4.4.1: croner@4.4.1:
resolution: {integrity: sha512-aqVeeIPCf5/NZFlz4mN4MLEOs9xf4ODCmHQDs+577JFj8mK3RkKJz77h7+Rn94AijUqKdFNOUHM+v88d8p02UQ==} resolution: {integrity: sha512-aqVeeIPCf5/NZFlz4mN4MLEOs9xf4ODCmHQDs+577JFj8mK3RkKJz77h7+Rn94AijUqKdFNOUHM+v88d8p02UQ==}
@ -3230,6 +3395,12 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkifyjs@4.3.1:
resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==}
lit-element@4.2.0: lit-element@4.2.0:
resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==} resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==}
@ -3332,6 +3503,10 @@ packages:
make-error@1.3.6: make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
markdown-table@3.0.4: markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@ -3389,6 +3564,9 @@ packages:
mdast-util-to-string@4.0.0: mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
media-typer@0.3.0: media-typer@0.3.0:
resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -3711,6 +3889,9 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
p-cancelable@3.0.0: p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@ -3896,6 +4077,64 @@ packages:
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
prosemirror-changeset@2.3.1:
resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==}
prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
prosemirror-commands@1.7.1:
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
prosemirror-gapcursor@1.3.2:
resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
prosemirror-history@1.4.1:
resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
prosemirror-inputrules@1.5.0:
resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
prosemirror-markdown@1.13.2:
resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==}
prosemirror-menu@1.2.5:
resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
prosemirror-model@1.25.1:
resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==}
prosemirror-schema-basic@1.2.4:
resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
prosemirror-state@1.4.3:
resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
prosemirror-tables@1.7.1:
resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==}
prosemirror-trailing-node@3.0.0:
resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
peerDependencies:
prosemirror-model: ^1.22.1
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
prosemirror-transform@1.10.4:
resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==}
prosemirror-view@1.40.0:
resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==}
proto-list@1.2.4: proto-list@1.2.4:
resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=} resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=}
@ -3923,6 +4162,10 @@ packages:
pumpify@1.5.1: pumpify@1.5.1:
resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@1.4.1: punycode@1.4.1:
resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=} resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=}
@ -4056,6 +4299,9 @@ packages:
resolution: {integrity: sha512-8svdqTMfF/LJ9ZS8NVT4pXAQDFXRrZFVyh9h+qbBprQ4Bge2dj1HkMl3b5LTJdvQY2ioWIBYsMBPw5TJ86j72Q==} resolution: {integrity: sha512-8svdqTMfF/LJ9ZS8NVT4pXAQDFXRrZFVyh9h+qbBprQ4Bge2dj1HkMl3b5LTJdvQY2ioWIBYsMBPw5TJ86j72Q==}
hasBin: true hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
rss-parser@3.13.0: rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
@ -4417,6 +4663,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
uglify-js@3.19.3: uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
@ -4490,6 +4739,9 @@ packages:
vfile@6.0.3: vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
webidl-conversions@7.0.0: webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -6766,6 +7018,8 @@ snapshots:
'@pushrocks/smarturl@2.0.1': {} '@pushrocks/smarturl@2.0.1': {}
'@remirror/core-constants@3.0.0': {}
'@rolldown/binding-darwin-arm64@1.0.0-beta.18': '@rolldown/binding-darwin-arm64@1.0.0-beta.18':
optional: true optional: true
@ -7223,6 +7477,154 @@ snapshots:
dependencies: dependencies:
stubborn-fs: 1.2.5 stubborn-fs: 1.2.5
'@tiptap/core@2.22.3(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/pm': 2.22.3
'@tiptap/extension-blockquote@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-bold@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-bullet-list@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-code-block@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-code@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-document@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-dropcursor@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-gapcursor@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-hard-break@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-heading@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-history@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-horizontal-rule@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-italic@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-link@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
linkifyjs: 4.3.1
'@tiptap/extension-list-item@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-ordered-list@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-paragraph@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-strike@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-text-align@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-text-style@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-text@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-typography@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-underline@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm@2.22.3':
dependencies:
prosemirror-changeset: 2.3.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.4.1
prosemirror-inputrules: 1.5.0
prosemirror-keymap: 1.2.3
prosemirror-markdown: 1.13.2
prosemirror-menu: 1.2.5
prosemirror-model: 1.25.1
prosemirror-schema-basic: 1.2.4
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.3
prosemirror-tables: 1.7.1
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
'@tiptap/starter-kit@2.22.3':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-blockquote': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-bold': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-bullet-list': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-code': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-code-block': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-document': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-dropcursor': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-gapcursor': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-hard-break': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-heading': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-history': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-horizontal-rule': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-italic': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-list-item': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-ordered-list': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-paragraph': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-strike': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-text': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-text-style': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/pm': 2.22.3
'@tokenizer/token@0.3.0': {} '@tokenizer/token@0.3.0': {}
'@tootallnate/quickjs-emscripten@0.23.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {}
@ -7418,10 +7820,19 @@ snapshots:
'@types/koa-compose': 3.2.8 '@types/koa-compose': 3.2.8
'@types/node': 22.15.31 '@types/node': 22.15.31
'@types/linkify-it@5.0.0': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/mdurl@2.0.0': {}
'@types/mime-types@2.1.4': {} '@types/mime-types@2.1.4': {}
'@types/mime@1.3.5': {} '@types/mime@1.3.5': {}
@ -8009,6 +8420,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
crelt@1.0.6: {}
croner@4.4.1: {} croner@4.4.1: {}
croner@5.7.0: {} croner@5.7.0: {}
@ -9071,6 +9484,12 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
linkifyjs@4.3.1: {}
lit-element@4.2.0: lit-element@4.2.0:
dependencies: dependencies:
'@lit-labs/ssr-dom-shim': 1.3.0 '@lit-labs/ssr-dom-shim': 1.3.0
@ -9178,6 +9597,15 @@ snapshots:
make-error@1.3.6: {} make-error@1.3.6: {}
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
markdown-table@3.0.4: {} markdown-table@3.0.4: {}
matcher@3.0.0: matcher@3.0.0:
@ -9317,6 +9745,8 @@ snapshots:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
mdurl@2.0.0: {}
media-typer@0.3.0: {} media-typer@0.3.0: {}
memory-pager@1.5.0: {} memory-pager@1.5.0: {}
@ -9721,6 +10151,8 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
orderedmap@2.1.1: {}
p-cancelable@3.0.0: {} p-cancelable@3.0.0: {}
p-event@4.2.0: p-event@4.2.0:
@ -9887,6 +10319,109 @@ snapshots:
property-information@7.1.0: {} property-information@7.1.0: {}
prosemirror-changeset@2.3.1:
dependencies:
prosemirror-transform: 1.10.4
prosemirror-collab@1.3.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-gapcursor@1.3.2:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
prosemirror-history@1.4.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.0:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.3
w3c-keyname: 2.2.8
prosemirror-markdown@1.13.2:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
prosemirror-model: 1.25.1
prosemirror-menu@1.2.5:
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.7.1
prosemirror-history: 1.4.1
prosemirror-state: 1.4.3
prosemirror-model@1.25.1:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-basic@1.2.4:
dependencies:
prosemirror-model: 1.25.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-state@1.4.3:
dependencies:
prosemirror-model: 1.25.1
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-tables@1.7.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
prosemirror-transform@1.10.4:
dependencies:
prosemirror-model: 1.25.1
prosemirror-view@1.40.0:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
proto-list@1.2.4: {} proto-list@1.2.4: {}
proxy-addr@2.0.7: proxy-addr@2.0.7:
@ -9932,6 +10467,8 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
pump: 2.0.1 pump: 2.0.1
punycode.js@2.3.1: {}
punycode@1.4.1: {} punycode@1.4.1: {}
punycode@2.3.1: {} punycode@2.3.1: {}
@ -10124,6 +10661,8 @@ snapshots:
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.18 '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.18
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.18 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.18
rope-sequence@1.3.4: {}
rss-parser@3.13.0: rss-parser@3.13.0:
dependencies: dependencies:
entities: 2.2.0 entities: 2.2.0
@ -10547,6 +11086,8 @@ snapshots:
typescript@5.8.3: {} typescript@5.8.3: {}
uc.micro@2.1.0: {}
uglify-js@3.19.3: {} uglify-js@3.19.3: {}
uint8array-extras@1.4.0: {} uint8array-extras@1.4.0: {}
@ -10619,6 +11160,8 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.2 vfile-message: 4.0.2
w3c-keyname@2.2.8: {}
webidl-conversions@7.0.0: {} webidl-conversions@7.0.0: {}
whatwg-mimetype@3.0.0: {} whatwg-mimetype@3.0.0: {}

View File

@ -513,4 +513,95 @@ Completed comprehensive refactoring to ensure clean, maintainable code with sepa
The refactoring follows the principles in instructions.md: The refactoring follows the principles in instructions.md:
- Uses static templates with manual DOM operations - Uses static templates with manual DOM operations
- Maintains separated concerns in different classes - Maintains separated concerns in different classes
- Results in clean, concise, and manageable code - Results in clean, concise, and manageable code
## Z-Index Management System (2025-12-24)
A comprehensive z-index management system has been implemented to fix overlay stacking conflicts:
### The Problem:
- Modals were hiding dropdown overlays
- Context menus appeared behind modals
- Inconsistent z-index values across components
- No clear hierarchy for overlay stacking
### The Solution:
#### 1. Central Z-Index Constants (`00zindex.ts`):
Created a centralized file defining all z-index layers:
```typescript
export const zIndexLayers = {
// Base layer: Regular content
base: {
content: 'auto',
inputElements: 1,
},
// Fixed UI elements
fixed: {
appBar: 10,
sideMenu: 10,
mobileNav: 250,
},
// Overlay backdrops
backdrop: {
dropdown: 1999,
modal: 2999,
contextMenu: 3999,
},
// Interactive overlays
overlay: {
dropdown: 2000, // Dropdowns and select menus
modal: 3000, // Modal dialogs
contextMenu: 4000, // Context menus and tooltips
toast: 5000, // Toast notifications
},
// Special cases
modalDropdown: 3500, // Dropdowns inside modals
wysiwygMenus: 4500, // Editor formatting menus
}
```
#### 2. Updated Components:
- **dees-modal**: Changed from 2000 to 3000
- **dees-windowlayer**: Changed from 200-201 to 1999-2000 (used by dropdowns)
- **dees-contextmenu**: Changed from 10000 to 4000
- **dees-toast**: Changed from 10000 to 5000
- **wysiwyg menus**: Changed from 10000 to 4500
- **dees-appui-profiledropdown**: Uses new dropdown z-index (2000)
#### 3. Stacking Order (bottom to top):
1. Regular page content (auto)
2. Fixed navigation elements (10-250)
3. Dropdown backdrop (1999)
4. Dropdown content (2000)
5. Modal backdrop (2999)
6. Modal content (3000)
7. Context menu (4000)
8. WYSIWYG menus (4500)
9. Toast notifications (5000)
#### 4. Key Benefits:
- Dropdowns now appear above modals
- Context menus appear above dropdowns and modals
- Toast notifications always appear on top
- Consistent and predictable stacking behavior
- Easy to adjust hierarchy by modifying central constants
#### 5. Testing:
Created `test-zindex.demo.ts` to verify stacking behavior with:
- Modal containing dropdown
- Context menu on modal
- Toast notifications
- Complex overlay combinations
### Usage:
Import and use the z-index constants in any component:
```typescript
import { zIndexLayers } from './00zindex.js';
// In styles
z-index: ${zIndexLayers.overlay.modal};
```
This system ensures proper stacking order for all overlay components and prevents z-index conflicts.

471
readme.md
View File

@ -12,14 +12,15 @@ npm install @design.estate/dees-catalog
| Category | Components | | Category | Components |
|----------|------------| |----------|------------|
| Core UI | `DeesButton`, `DeesBadge`, `DeesChips`, `DeesIcon`, `DeesLabel`, `DeesSpinner`, `DeesToast` | | Core UI | `DeesButton`, `DeesButtonExit`, `DeesButtonGroup`, `DeesBadge`, `DeesChips`, `DeesHeading`, `DeesHint`, `DeesIcon`, `DeesLabel`, `DeesPanel`, `DeesSearchbar`, `DeesSpinner`, `DeesToast`, `DeesWindowcontrols` |
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadio`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesFormSubmit` | | Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadiogroup`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesInputTags`, `DeesInputTypelist`, `DeesInputRichtext`, `DeesInputWysiwyg`, `DeesFormSubmit` |
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesMobileNavigation` | | Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesAppuiActivitylog`, `DeesAppuiProfiledropdown`, `DeesAppuiTabs`, `DeesAppuiView`, `DeesMobileNavigation` |
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid` | | Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid`, `DeesPagination` |
| Visualization | `DeesChartArea`, `DeesChartLog` | | Visualization | `DeesChartArea`, `DeesChartLog` |
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` | | Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` |
| Navigation | `DeesStepper`, `DeesProgressbar` | | Navigation | `DeesStepper`, `DeesProgressbar`, `DeesMobileNavigation` |
| Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesTerminal`, `DeesUpdater` | | Development | `DeesEditor`, `DeesEditorMarkdown`, `DeesEditorMarkdownoutlet`, `DeesTerminal`, `DeesUpdater` |
| Auth & Utilities | `DeesSimpleAppdash`, `DeesSimpleLogin` |
## Detailed Component Documentation ## Detailed Component Documentation
@ -149,6 +150,93 @@ Key Features:
- Theme-aware styling - Theme-aware styling
- Programmatic control - Programmatic control
#### `DeesButtonExit`
Exit/close button component with consistent styling.
```typescript
<dees-button-exit
@click=${handleClose}
></dees-button-exit>
```
#### `DeesButtonGroup`
Container for grouping related buttons together.
```typescript
<dees-button-group
.buttons=${[
{ text: 'Save', type: 'highlighted', action: handleSave },
{ text: 'Cancel', type: 'normal', action: handleCancel }
]}
spacing="medium" // Options: small, medium, large
></dees-button-group>
```
#### `DeesHeading`
Consistent heading component with level and styling options.
```typescript
<dees-heading
level={1} // 1-6 for H1-H6
text="Page Title"
.subheading=${'Optional subtitle'}
centered // Optional: center alignment
></dees-heading>
```
#### `DeesHint`
Hint/tooltip component for providing contextual help.
```typescript
<dees-hint
text="This field is required"
type="info" // Options: info, warning, error, success
position="top" // Options: top, bottom, left, right
></dees-hint>
```
#### `DeesPanel`
Container component for grouping related content with optional title and actions.
```typescript
<dees-panel
.title=${'Panel Title'}
.subtitle=${'Optional subtitle'}
collapsible // Optional: allow collapse/expand
collapsed={false} // Initial collapsed state
.actions=${[
{ icon: 'settings', action: handleSettings }
]}
>
<!-- Panel content -->
</dees-panel>
```
#### `DeesSearchbar`
Search input component with suggestions and search handling.
```typescript
<dees-searchbar
placeholder="Search..."
.suggestions=${['item1', 'item2', 'item3']}
showClearButton // Show clear button when has value
@search=${handleSearch}
@suggestion-select=${handleSuggestionSelect}
></dees-searchbar>
```
#### `DeesWindowcontrols`
Window control buttons (minimize, maximize, close) for desktop-like applications.
```typescript
<dees-windowcontrols
.controls=${['minimize', 'maximize', 'close']}
@minimize=${handleMinimize}
@maximize=${handleMaximize}
@close=${handleClose}
></dees-windowcontrols>
```
### Form Components ### Form Components
#### `DeesForm` #### `DeesForm`
@ -207,22 +295,6 @@ Dropdown selection component with search and filtering capabilities.
></dees-input-dropdown> ></dees-input-dropdown>
``` ```
#### `DeesInputRadio`
Radio button group for single-choice selections.
```typescript
<dees-input-radio
key="gender"
label="Gender"
.options=${[
{ key: 'male', option: 'Male' },
{ key: 'female', option: 'Female' },
{ key: 'other', option: 'Other' }
]}
required
></dees-input-radio>
```
#### `DeesInputFileupload` #### `DeesInputFileupload`
File upload component with drag-and-drop support. File upload component with drag-and-drop support.
@ -293,6 +365,121 @@ Multi-state toggle button group.
></dees-input-multitoggle> ></dees-input-multitoggle>
``` ```
#### `DeesInputRadiogroup`
Radio button group for single-choice selections with internal state management.
```typescript
<dees-input-radiogroup
key="plan"
label="Select Plan"
.options=${['Free', 'Pro', 'Enterprise']}
selectedOption="Pro"
required
@change=${handlePlanChange}
></dees-input-radiogroup>
// With custom option objects
<dees-input-radiogroup
key="priority"
label="Priority Level"
.options=${[
{ key: 'low', label: 'Low Priority' },
{ key: 'medium', label: 'Medium Priority' },
{ key: 'high', label: 'High Priority' }
]}
selectedOption="medium"
></dees-input-radiogroup>
```
#### `DeesInputTags`
Tag input component for managing lists of tags with auto-complete and validation.
```typescript
<dees-input-tags
key="skills"
label="Skills"
.value=${['JavaScript', 'TypeScript', 'CSS']}
placeholder="Add a skill..."
.suggestions=${[
'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust',
'React', 'Vue', 'Angular', 'Node.js', 'Docker'
]}
maxTags={10} // Optional: limit number of tags
required
@change=${handleTagsChange}
></dees-input-tags>
```
Key Features:
- Add tags by pressing Enter or typing comma/semicolon
- Remove tags with click or backspace
- Auto-complete suggestions with keyboard navigation
- Maximum tag limit support
- Full theme support
- Form validation integration
#### `DeesInputTypelist`
Dynamic list input for managing arrays of typed values.
```typescript
<dees-input-typelist
key="features"
label="Product Features"
placeholder="Add a feature..."
.value=${['Feature 1', 'Feature 2']}
@change=${handleFeaturesChange}
></dees-input-typelist>
```
#### `DeesInputRichtext`
Rich text editor with formatting toolbar powered by TipTap.
```typescript
<dees-input-richtext
key="content"
label="Article Content"
.value=${htmlContent}
placeholder="Start writing..."
minHeight={300} // Minimum editor height
showWordCount={true} // Show word/character count
@change=${handleContentChange}
></dees-input-richtext>
```
Key Features:
- Full formatting toolbar (bold, italic, underline, strike, etc.)
- Heading levels (H1-H6)
- Lists (bullet, ordered)
- Links with URL editing
- Code blocks and inline code
- Blockquotes
- Horizontal rules
- Undo/redo support
- Word and character count
- HTML output
#### `DeesInputWysiwyg`
Advanced block-based editor with slash commands and rich content blocks.
```typescript
<dees-input-wysiwyg
key="document"
label="Document Editor"
.value=${documentContent}
outputFormat="html" // Options: html, markdown, json
@change=${handleDocumentChange}
></dees-input-wysiwyg>
```
Key Features:
- Slash commands for quick formatting
- Block-based editing (paragraphs, headings, lists, etc.)
- Drag and drop block reordering
- Multiple output formats
- Keyboard shortcuts
- Collaborative editing ready
- Extensible block types
#### `DeesFormSubmit` #### `DeesFormSubmit`
Submit button component specifically designed for `DeesForm`. Submit button component specifically designed for `DeesForm`.
@ -622,6 +809,91 @@ Best Practices:
- Test with screen readers - Test with screen readers
- Maintain focus management - Maintain focus management
#### `DeesAppuiActivitylog`
Activity log component for displaying system events and user actions.
```typescript
<dees-appui-activitylog
.entries=${[
{
timestamp: new Date(),
type: 'info',
message: 'User logged in',
details: { userId: '123' }
},
{
timestamp: new Date(),
type: 'error',
message: 'Failed to save document',
details: { error: 'Network error' }
}
]}
maxEntries={100} // Maximum entries to display
@entry-click=${handleEntryClick}
></dees-appui-activitylog>
```
#### `DeesAppuiProfiledropdown`
User profile dropdown component with status and menu options.
```typescript
<dees-appui-profiledropdown
.user=${{
name: 'John Doe',
email: 'john@example.com',
avatar: '/path/to/avatar.jpg',
status: 'online' // Options: online, offline, busy, away
}}
.menuItems=${[
{ name: 'Profile', iconName: 'user', action: async () => {} },
{ name: 'Settings', iconName: 'settings', action: async () => {} },
{ divider: true },
{ name: 'Logout', iconName: 'logOut', action: async () => {} }
]}
@status-change=${handleStatusChange}
></dees-appui-profiledropdown>
```
#### `DeesAppuiTabs`
Tab navigation component for organizing content sections.
```typescript
<dees-appui-tabs
.tabs=${[
{
key: 'overview',
label: 'Overview',
icon: 'home',
content: html`<div>Overview content</div>`
},
{
key: 'details',
label: 'Details',
icon: 'info',
content: html`<div>Details content</div>`
}
]}
selectedTab="overview"
@tab-change=${handleTabChange}
></dees-appui-tabs>
```
#### `DeesAppuiView`
View container component for consistent page layouts.
```typescript
<dees-appui-view
viewTitle="Dashboard"
viewSubtitle="System Overview"
.headerActions=${[
{ icon: 'refresh', action: handleRefresh },
{ icon: 'settings', action: handleSettings }
]}
>
<!-- View content -->
</dees-appui-view>
```
#### `DeesMobileNavigation` #### `DeesMobileNavigation`
Responsive navigation component for mobile devices. Responsive navigation component for mobile devices.
@ -982,6 +1254,27 @@ setInterval(() => {
}, 3000); }, 3000);
``` ```
#### `DeesPagination`
Pagination component for navigating through large datasets.
```typescript
<dees-pagination
totalItems={500}
itemsPerPage={20}
currentPage={1}
maxVisiblePages={7} // Maximum page numbers to display
@page-change=${handlePageChange}
></dees-pagination>
```
Key Features:
- Page number navigation
- Previous/next buttons
- Jump to first/last page
- Configurable items per page
- Responsive design
- Keyboard navigation support
### Visualization Components ### Visualization Components
#### `DeesChartArea` #### `DeesChartArea`
@ -1306,52 +1599,6 @@ Key Features:
- Animation support - Animation support
- Accessibility features - Accessibility features
#### `DeesMobileNavigation`
Mobile-optimized navigation component with touch support.
```typescript
// Programmatic usage
DeesMobilenavigation.createAndShow([
{
name: 'Home',
action: async (nav) => {
// Handle navigation
return null;
}
},
{
name: 'Settings',
action: async (nav) => {
// Handle navigation
return null;
}
}
]);
// Component usage
<dees-mobilenavigation
heading="MENU"
.menuItems=${[
{
name: 'Profile',
action: (nav) => handleNavigation('profile')
},
{
name: 'Settings',
action: (nav) => handleNavigation('settings')
}
]}
></dees-mobilenavigation>
```
Key Features:
- Touch-friendly interface
- Slide-in animation
- Backdrop overlay
- Single instance management
- Custom menu items
- Responsive design
Best Practices: Best Practices:
1. Stepper Implementation 1. Stepper Implementation
@ -1368,13 +1615,6 @@ Best Practices:
- Performance monitoring - Performance monitoring
- Error state handling - Error state handling
3. Mobile Navigation
- Touch-optimized targets
- Clear visual hierarchy
- Smooth animations
- Gesture support
- Responsive behavior
Common Use Cases: Common Use Cases:
1. Stepper 1. Stepper
@ -1391,13 +1631,6 @@ Common Use Cases:
- Task completion - Task completion
- Step progression - Step progression
3. Mobile Navigation
- Responsive menus
- App navigation
- Settings access
- User actions
- Context switching
Accessibility Considerations: Accessibility Considerations:
- Keyboard navigation support - Keyboard navigation support
- ARIA labels and roles - ARIA labels and roles
@ -1461,6 +1694,26 @@ Key Features:
- Spellcheck integration - Spellcheck integration
- Auto-save functionality - Auto-save functionality
#### `DeesEditorMarkdownoutlet`
Markdown preview component for rendering markdown content.
```typescript
<dees-editor-markdownoutlet
.markdown=${markdownContent}
.theme=${'github'} // Options: github, dark, custom
.plugins=${['mermaid', 'highlight']} // Optional plugins
allowHtml={false} // Security: disable raw HTML
></dees-editor-markdownoutlet>
```
Key Features:
- Safe markdown rendering
- Multiple themes
- Plugin support (mermaid diagrams, syntax highlighting)
- XSS protection
- Custom CSS injection
- Responsive images
#### `DeesTerminal` #### `DeesTerminal`
Terminal emulator component for command-line interface. Terminal emulator component for command-line interface.
@ -1606,6 +1859,60 @@ Accessibility Features:
- Focus management - Focus management
- ARIA attributes - ARIA attributes
### Auth & Utilities Components
#### `DeesSimpleAppdash`
Simple application dashboard component for quick prototyping.
```typescript
<dees-simple-appdash
.appTitle=${'My Application'}
.menuItems=${[
{ name: 'Dashboard', icon: 'home', route: '/dashboard' },
{ name: 'Settings', icon: 'settings', route: '/settings' }
]}
.user=${{
name: 'John Doe',
role: 'Administrator'
}}
@menu-select=${handleMenuSelect}
>
<!-- Dashboard content -->
</dees-simple-appdash>
```
Key Features:
- Quick setup dashboard layout
- Built-in navigation
- User profile section
- Responsive design
- Minimal configuration
#### `DeesSimpleLogin`
Simple login form component with validation and customization.
```typescript
<dees-simple-login
.appName=${'My Application'}
.logo=${'./assets/logo.png'}
.backgroundImage=${'./assets/background.jpg'}
.fields=${['username', 'password']} // Options: username, email, password
showForgotPassword
showRememberMe
@login=${handleLogin}
@forgot-password=${handleForgotPassword}
></dees-simple-login>
```
Key Features:
- Customizable fields
- Built-in validation
- Remember me option
- Forgot password link
- Custom branding
- Responsive layout
- Loading states
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository. This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.

161
ts_web/elements/00zindex.ts Normal file
View File

@ -0,0 +1,161 @@
/**
* Central z-index management for consistent stacking order
* Higher numbers appear on top of lower numbers
*/
export const zIndexLayers = {
// Base layer: Regular content
base: {
content: 'auto',
inputElements: 1,
},
// Fixed UI elements
fixed: {
appBar: 10,
sideMenu: 10,
mobileNav: 250,
},
// Overlay backdrops (semi-transparent backgrounds)
backdrop: {
dropdown: 1999, // Below modals but above fixed elements
modal: 2999, // Below dropdowns on modals
contextMenu: 3999, // Below critical overlays
},
// Interactive overlays
overlay: {
dropdown: 2000, // Dropdowns and select menus
modal: 3000, // Modal dialogs
contextMenu: 4000, // Context menus and tooltips
toast: 5000, // Toast notifications (highest priority)
},
// Special cases for nested elements
modalDropdown: 3500, // Dropdowns inside modals
wysiwygMenus: 4500, // Editor formatting menus
} as const;
// Helper function to get z-index value
export function getZIndex(category: keyof typeof zIndexLayers, subcategory?: string): number | string {
const categoryObj = zIndexLayers[category];
if (typeof categoryObj === 'object' && subcategory) {
return categoryObj[subcategory as keyof typeof categoryObj] || 'auto';
}
return typeof categoryObj === 'number' ? categoryObj : 'auto';
}
// Z-index assignments for components
export const componentZIndex = {
'dees-modal': zIndexLayers.overlay.modal,
'dees-windowlayer': zIndexLayers.overlay.dropdown,
'dees-contextmenu': zIndexLayers.overlay.contextMenu,
'dees-toast': zIndexLayers.overlay.toast,
'dees-appui-mainmenu': zIndexLayers.fixed.appBar,
'dees-mobilenavigation': zIndexLayers.fixed.mobileNav,
'dees-slash-menu': zIndexLayers.wysiwygMenus,
'dees-formatting-menu': zIndexLayers.wysiwygMenus,
} as const;
/**
* Z-Index Registry for managing stacked elements
* Simple incremental z-index assignment based on creation order
*/
export class ZIndexRegistry {
private static instance: ZIndexRegistry;
private activeElements = new Set<HTMLElement>();
private elementZIndexMap = new WeakMap<HTMLElement, number>();
private currentZIndex = 1000; // Starting z-index
private constructor() {}
public static getInstance(): ZIndexRegistry {
if (!ZIndexRegistry.instance) {
ZIndexRegistry.instance = new ZIndexRegistry();
}
return ZIndexRegistry.instance;
}
/**
* Get the next available z-index
* @returns The next available z-index
*/
public getNextZIndex(): number {
this.currentZIndex += 10;
return this.currentZIndex;
}
/**
* Register an element with the z-index registry
* @param element - The HTML element to register
* @param zIndex - The z-index assigned to this element
*/
public register(element: HTMLElement, zIndex: number): void {
this.activeElements.add(element);
this.elementZIndexMap.set(element, zIndex);
}
/**
* Unregister an element from the z-index registry
* @param element - The HTML element to unregister
*/
public unregister(element: HTMLElement): void {
this.activeElements.delete(element);
this.elementZIndexMap.delete(element);
// If no more active elements, reset counter to base
if (this.activeElements.size === 0) {
this.currentZIndex = 1000;
}
}
/**
* Get the z-index for a specific element
* @param element - The HTML element
* @returns The z-index or undefined if not registered
*/
public getElementZIndex(element: HTMLElement): number | undefined {
return this.elementZIndexMap.get(element);
}
/**
* Get count of active elements
* @returns Number of active elements
*/
public getActiveCount(): number {
return this.activeElements.size;
}
/**
* Get the current highest z-index
* @returns The current z-index value
*/
public getCurrentZIndex(): number {
return this.currentZIndex;
}
/**
* Clear all registrations (useful for testing)
*/
public clear(): void {
this.activeElements.clear();
this.elementZIndexMap = new WeakMap();
this.currentZIndex = 1000;
}
/**
* Get all active elements in z-index order
* @returns Array of elements sorted by z-index
*/
public getActiveElementsInOrder(): HTMLElement[] {
return Array.from(this.activeElements).sort((a, b) => {
const aZ = this.elementZIndexMap.get(a) || 0;
const bZ = this.elementZIndexMap.get(b) || 0;
return aZ - bZ;
});
}
}
// Export singleton instance for convenience
export const zIndexRegistry = ZIndexRegistry.getInstance();

View File

@ -1,5 +1,6 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import * as interfaces from './interfaces/index.js'; import * as interfaces from './interfaces/index.js';
import { zIndexLayers } from './00zindex.js';
import { import {
DeesElement, DeesElement,
@ -46,7 +47,7 @@ export class DeesAppuiMainmenu extends DeesElement {
.mainContainer { .mainContainer {
--menuSize: 60px; --menuSize: 60px;
color: ${cssManager.bdTheme('#666', '#ccc')}; color: ${cssManager.bdTheme('#666', '#ccc')};
z-index: 10; z-index: ${zIndexLayers.fixed.appBar};
display: block; display: block;
position: relative; position: relative;
width: var(--menuSize); width: var(--menuSize);

View File

@ -1,4 +1,5 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js';
import { import {
DeesElement, DeesElement,
@ -73,7 +74,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
'0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.3)' '0 4px 12px rgba(0, 0, 0, 0.3)'
)}; )};
z-index: 1000; z-index: ${zIndexLayers.overlay.dropdown};
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(-10px); transform: scale(0.95) translateY(-10px);
transition: opacity 0.2s, transform 0.2s; transition: opacity 0.2s, transform 0.2s;
@ -258,7 +259,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
z-index: 999; z-index: ${zIndexLayers.backdrop.dropdown};
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
display: none; display: none;

View File

@ -14,6 +14,7 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from './dees-windowlayer.js'; import { DeesWindowLayer } from './dees-windowlayer.js';
import { zIndexLayers } from './00zindex.js';
import './dees-icon.js'; import './dees-icon.js';
declare global { declare global {
@ -74,7 +75,7 @@ export class DeesContextmenu extends DeesElement {
eventArg.stopPropagation(); eventArg.stopPropagation();
const contextMenu = new DeesContextmenu(); const contextMenu = new DeesContextmenu();
contextMenu.style.position = 'fixed'; contextMenu.style.position = 'fixed';
contextMenu.style.zIndex = '10000'; contextMenu.style.zIndex = String(zIndexLayers.overlay.contextMenu);
contextMenu.style.opacity = '0'; contextMenu.style.opacity = '0';
contextMenu.style.transform = 'scale(0.95) translateY(-10px)'; contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
contextMenu.menuItems = menuItemsArg; contextMenu.menuItems = menuItemsArg;

View File

@ -11,7 +11,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { DeesInputCheckbox } from './dees-input-checkbox.js'; import { DeesInputCheckbox } from './dees-input-checkbox.js';
import { DeesInputText } from './dees-input-text.js'; import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadio } from './dees-input-radio.js'; import { DeesInputRadiogroup } from './dees-input-radiogroup.js';
import { DeesInputDropdown } from './dees-input-dropdown.js'; import { DeesInputDropdown } from './dees-input-dropdown.js';
import { DeesInputFileupload } from './dees-input-fileupload.js'; import { DeesInputFileupload } from './dees-input-fileupload.js';
import { DeesInputIban } from './dees-input-iban.js'; import { DeesInputIban } from './dees-input-iban.js';
@ -31,7 +31,7 @@ const FORM_INPUT_TYPES = [
DeesInputMultitoggle, DeesInputMultitoggle,
DeesInputPhone, DeesInputPhone,
DeesInputQuantitySelector, DeesInputQuantitySelector,
DeesInputRadio, DeesInputRadiogroup,
DeesInputText, DeesInputText,
DeesInputTypelist, DeesInputTypelist,
DeesTable, DeesTable,
@ -45,7 +45,7 @@ export type TFormInputElement =
| DeesInputMultitoggle | DeesInputMultitoggle
| DeesInputPhone | DeesInputPhone
| DeesInputQuantitySelector | DeesInputQuantitySelector
| DeesInputRadio | DeesInputRadiogroup
| DeesInputText | DeesInputText
| DeesInputTypelist | DeesInputTypelist
| DeesTable<any>; | DeesTable<any>;
@ -132,7 +132,6 @@ export class DeesForm extends DeesElement {
public async collectFormData() { public async collectFormData() {
const children = this.getFormElements(); const children = this.getFormElements();
const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {}; const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {};
const radioGroups = new Map<string, DeesInputRadio[]>();
for (const child of children) { for (const child of children) {
if (!child.key) { if (!child.key) {
@ -140,21 +139,7 @@ export class DeesForm extends DeesElement {
continue; continue;
} }
// Handle radio buttons specially valueObject[child.key] = child.value;
if (child instanceof DeesInputRadio && child.name) {
if (!radioGroups.has(child.name)) {
radioGroups.set(child.name, []);
}
radioGroups.get(child.name).push(child);
} else {
valueObject[child.key] = child.value;
}
}
// Process radio groups - use the name as key and selected radio's key as value
for (const [groupName, radios] of radioGroups) {
const selectedRadio = radios.find(radio => radio.value === true);
valueObject[groupName] = selectedRadio ? selectedRadio.key : null;
} }
return valueObject; return valueObject;

View File

@ -50,14 +50,24 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
} }
.maincontainer { .maincontainer {
padding: 5px 0px; display: flex;
align-items: center;
gap: 12px;
padding: 8px 0px;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
cursor: pointer;
user-select: none;
transition: all 0.2s;
} }
.maincontainer:hover { .maincontainer:hover {
color: ${cssManager.bdTheme('#000', '#fff')}; color: ${cssManager.bdTheme('#000', '#fff')};
} }
.maincontainer:hover .checkbox {
border-color: ${cssManager.bdTheme('#999', '#888')};
}
input:focus { input:focus {
outline: none; outline: none;
border-bottom: 1px solid #e4002b; border-bottom: 1px solid #e4002b;
@ -72,6 +82,7 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
width: 24px; width: 24px;
display: inline-block; display: inline-block;
background: ${cssManager.bdTheme('#fafafa', '#222')}; background: ${cssManager.bdTheme('#fafafa', '#222')};
flex-shrink: 0;
} }
.checkbox.selected { .checkbox.selected {
@ -118,13 +129,43 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
img { img {
padding: 4px; padding: 4px;
} }
.checkbox-label {
font-size: 14px;
transition: color 0.2s ease;
}
.maincontainer:hover .checkbox-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.maincontainer.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.maincontainer.disabled:hover {
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.maincontainer.disabled:hover .checkbox {
border-color: ${cssManager.bdTheme('#ccc', '#333')};
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 4px;
line-height: 1.4;
padding-left: 36px;
}
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}"> <div class="maincontainer ${this.disabled ? 'disabled' : ''}" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0"> <div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
${this.value ${this.value
? html` ? html`
@ -135,8 +176,11 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
` `
: html``} : html``}
</div> </div>
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
</div> </div>
<dees-label .label=${this.label}></dees-label> ${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
</div> </div>
`; `;
} }

View File

@ -280,6 +280,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px'; elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px';
elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px'; elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px';
elevatedDropdown.style.width = this.clientWidth + 'px'; elevatedDropdown.style.width = this.clientWidth + 'px';
// Get z-index from registry for the elevated dropdown
const dropdownZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
elevatedDropdown.style.zIndex = dropdownZIndex.toString();
(await import('./00zindex.js')).zIndexRegistry.register(elevatedDropdown, dropdownZIndex);
elevatedDropdown.options = this.options; elevatedDropdown.options = this.options;
elevatedDropdown.selectedOption = this.selectedOption; elevatedDropdown.selectedOption = this.selectedOption;
elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf( elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf(
@ -296,9 +301,13 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
'0'; '0';
elevatedDropdown.removeEventListener('selectedOption', handleSelection); elevatedDropdown.removeEventListener('selectedOption', handleSelection);
this.windowOverlay.removeEventListener('clicked', destroyOverlay); this.windowOverlay.removeEventListener('clicked', destroyOverlay);
// Unregister elevated dropdown from z-index registry
(await import('./00zindex.js')).zIndexRegistry.unregister(elevatedDropdown);
this.windowOverlay.destroy(); this.windowOverlay.destroy();
}; };
const handleSelection = async (event) => { const handleSelection = async () => {
await this.updateSelection(elevatedDropdown.selectedOption); await this.updateSelection(elevatedDropdown.selectedOption);
destroyOverlay(); destroyOverlay();
}; };
@ -323,10 +332,20 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
await domtoolsInstance.convenience.smartdelay.delayFor(0); await domtoolsInstance.convenience.smartdelay.delayFor(0);
const searchInput = selectionBox.querySelector('input'); const searchInput = selectionBox.querySelector('input');
searchInput?.focus(); searchInput?.focus();
// Get z-index from registry for the selection box
const selectionBoxZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex();
selectionBox.style.zIndex = selectionBoxZIndex.toString();
(await import('./00zindex.js')).zIndexRegistry.register(selectionBox as HTMLElement, selectionBoxZIndex);
selectionBox.classList.add('show'); selectionBox.classList.add('show');
} else { } else {
selectedBox.style.pointerEvents = 'none'; selectedBox.style.pointerEvents = 'none';
selectionBox.classList.remove('show'); selectionBox.classList.remove('show');
// Unregister selection box from z-index registry
(await import('./00zindex.js')).zIndexRegistry.unregister(selectionBox as HTMLElement);
// selectedBox.style.opacity = '0'; // selectedBox.style.opacity = '0';
} }
} }

View File

@ -51,85 +51,151 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}> <dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
<dees-input-fileupload <dees-input-fileupload
.label=${'Attachments'} .label=${'Attachments'}
.description=${'Upload files by clicking or dragging'} .description=${'Upload any files by clicking or dragging them here'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Single File Only'}
.description=${'Upload your CV in PDF format'} .description=${'Only one file can be uploaded at a time'}
.buttonText=${'Choose Resume...'} .multiple=${false}
.buttonText=${'Choose File'}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Multiple Upload Areas'} .subtitle=${'Different upload zones for various file types'}> <dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}>
<div class="upload-grid"> <div class="upload-grid">
<div class="upload-box"> <div class="upload-box">
<h4>Profile Picture</h4> <h4>Images Only</h4>
<dees-input-fileupload <dees-input-fileupload
.label=${'Avatar'} .label=${'Profile Picture'}
.description=${'JPG, PNG or GIF'} .description=${'JPG, PNG or GIF (max 5MB)'}
.buttonText=${'Select Image...'} .accept=${'image/jpeg,image/png,image/gif'}
.maxSize=${5 * 1024 * 1024}
.multiple=${false}
.buttonText=${'Select Image'}
></dees-input-fileupload> ></dees-input-fileupload>
</div> </div>
<div class="upload-box"> <div class="upload-box">
<h4>Cover Image</h4> <h4>Documents Only</h4>
<dees-input-fileupload <dees-input-fileupload
.label=${'Banner'} .label=${'Resume'}
.description=${'Recommended: 1200x400px'} .description=${'PDF or Word documents only'}
.buttonText=${'Select Banner...'} .accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
.buttonText=${'Select Document'}
></dees-input-fileupload> ></dees-input-fileupload>
</div> </div>
</div> </div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different upload states for validation'}> <dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}>
<dees-input-fileupload <dees-input-fileupload
.label=${'Identity Document'} .label=${'Small Files Only'}
.description=${'Required for verification'} .description=${'Maximum file size: 1MB'}
.required=${true} .maxSize=${1024 * 1024}
.buttonText=${'Upload Document...'} .buttonText=${'Upload Small File'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'System Files'} .label=${'Limited Upload'}
.description=${'File upload is disabled'} .description=${'Maximum 3 files, each up to 2MB'}
.disabled=${true} .maxFiles=${3}
.value=${[]} .maxSize=${2 * 1024 * 1024}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Required Upload'}
.description=${'This field is required'}
.required=${true}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Application Form'} .subtitle=${'Complete form with file upload integration'}> <dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}>
<dees-input-fileupload
.label=${'Disabled Upload'}
.description=${'File upload is currently disabled'}
.disabled=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Pre-filled Example'}
.description=${'Component with pre-loaded files'}
.value=${[
new File(['Hello World'], 'example.txt', { type: 'text/plain' }),
new File(['Test Data'], 'data.json', { type: 'application/json' })
]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}>
<dees-form> <dees-form>
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text> <h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3>
<dees-input-text .label=${'Email'} .inputType=${'email'} .required=${true}></dees-input-text>
<dees-input-text
.label=${'Full Name'}
.required=${true}
.key=${'fullName'}
></dees-input-text>
<dees-input-text
.label=${'Email'}
.inputType=${'email'}
.required=${true}
.key=${'email'}
></dees-input-text>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Resume'}
.description=${'Upload your CV (PDF preferred)'} .description=${'Required: PDF format only (max 10MB)'}
.required=${true} .required=${true}
.accept=${'application/pdf'}
.maxSize=${10 * 1024 * 1024}
.multiple=${false}
.key=${'resume'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'Portfolio'} .label=${'Portfolio'}
.description=${'Optional: Upload work samples'} .description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'}
.accept=${'image/*,application/pdf'}
.maxFiles=${5}
.maxSize=${5 * 1024 * 1024}
.key=${'portfolio'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload
.label=${'References'}
.description=${'Upload reference letters (optional)'}
.accept=${".pdf,.doc,.docx"}
.key=${'references'}
></dees-input-fileupload>
<dees-input-text <dees-input-text
.label=${'Cover Letter'} .label=${'Additional Comments'}
.inputType=${'textarea'} .inputType=${'textarea'}
.description=${'Tell us why you would be a great fit'} .description=${'Any additional information you would like to share'}
.key=${'comments'}
></dees-input-text> ></dees-input-text>
<dees-form-submit .text=${'Submit Application'}></dees-form-submit>
</dees-form> </dees-form>
<div class="info-section"> <div class="info-section">
<h4>Features:</h4> <h4 style="margin-top: 0;">Enhanced Features:</h4>
<ul> <ul style="margin: 0; padding-left: 20px;">
<li>Click to select files or drag & drop</li> <li>Drag & drop with visual feedback</li>
<li>Multiple file selection support</li> <li>File type restrictions via accept attribute</li>
<li>Visual feedback for drag operations</li> <li>File size validation with custom limits</li>
<li>Right-click files to remove them</li> <li>Maximum file count restrictions</li>
<li>Integrates seamlessly with forms</li> <li>Image preview thumbnails</li>
<li>File type-specific icons</li>
<li>Clear all button for multiple files</li>
<li>Proper validation states and messages</li>
<li>Keyboard accessible</li>
<li>Single or multiple file modes</li>
</ul> </ul>
</div> </div>
</dees-panel> </dees-panel>

View File

@ -42,6 +42,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}) })
public buttonText: string = 'Upload File...'; public buttonText: string = 'Upload File...';
@property({ type: String })
public accept: string = '';
@property({ type: Boolean })
public multiple: boolean = true;
@property({ type: Number })
public maxSize: number = 0; // 0 means no limit
@property({ type: Number })
public maxFiles: number = 0; // 0 means no limit
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
constructor() { constructor() {
super(); super();
} }
@ -52,7 +67,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
css` css`
:host { :host {
position: relative; position: relative;
display: grid; display: block;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
} }
@ -60,13 +75,42 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
display: none; display: none;
} }
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.maincontainer { .maincontainer {
position: relative; position: relative;
border-radius: 3px; border-radius: 8px;
padding: 8px; padding: 16px;
background: ${cssManager.bdTheme('#fafafa', '#222222')}; background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
border-top: 1px solid #ffffff10; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
transition: all 0.2s ease;
}
.maincontainer:hover {
border-color: ${cssManager.bdTheme('#ccc', '#444')};
}
:host([disabled]) .maincontainer {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
:host([validationState="invalid"]) .maincontainer {
border-color: #e74c3c;
}
:host([validationState="valid"]) .maincontainer {
border-color: #27ae60;
}
:host([validationState="warn"]) .maincontainer {
border-color: #f39c12;
} }
.maincontainer::after { .maincontainer::after {
@ -78,115 +122,385 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
position: absolute; position: absolute;
content: ''; content: '';
display: block; display: block;
border: 2px dashed rgba(255, 255, 255, 0); border: 2px dashed transparent;
transition: all 0.2s; border-radius: 6px;
transition: all 0.3s ease;
pointer-events: none; pointer-events: none;
background: #00000000; background: transparent;
} }
.maincontainer.dragOver {
border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: ${cssManager.bdTheme('#f0f8ff', '#001933')};
}
.maincontainer.dragOver::after { .maincontainer.dragOver::after {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
border: 2px dashed rgba(255, 255, 255, 0.3); border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: #00000080;
} }
.uploadButton { .uploadButton {
position: relative; position: relative;
padding: 8px; padding: 12px 24px;
max-width: 600px; background: ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: ${cssManager.bdTheme('#fafafa', '#333333')}; color: white;
border-radius: 3px; border-radius: 6px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
cursor: default; font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
} }
.uploadButton:hover { .uploadButton:hover {
color: #fff; background: ${cssManager.bdTheme('#0073e6', '#0073e6')};
background: ${unsafeCSS(colors.dark.blue)}; transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3);
}
.uploadButton:active {
transform: translateY(0);
}
.uploadButton dees-icon {
font-size: 16px;
}
.files-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
} }
.uploadCandidate { .uploadCandidate {
display: grid; display: grid;
grid-template-columns: 48px auto; grid-template-columns: 40px 1fr auto;
background: #333; background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')};
padding: 8px 8px 8px 0px; padding: 12px;
margin-bottom: 8px;
text-align: left; text-align: left;
border-radius: 3px; border-radius: 6px;
color: ${cssManager.bdTheme('#666', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
font-family: 'Geist Sans', sans-serif;
cursor: default; cursor: default;
transition: all 0.2s; transition: all 0.2s;
border-top: 1px solid #ffffff10; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
position: relative;
overflow: hidden;
} }
.uploadCandidate:last-child { .uploadCandidate:hover {
margin-bottom: 8px; background: ${cssManager.bdTheme('#f5f5f5', '#333')};
border-color: ${cssManager.bdTheme('#ccc', '#444')};
} }
.uploadCandidate .icon { .uploadCandidate .icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')};
} }
.uploadCandidate:hover { .uploadCandidate.image-file .icon {
background: #393939; color: #4CAF50;
}
.uploadCandidate.pdf-file .icon {
color: #f44336;
}
.uploadCandidate.doc-file .icon {
color: #2196F3;
} }
.uploadCandidate .description { .uploadCandidate .info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.uploadCandidate .filename {
font-weight: 500;
font-size: 14px; font-size: 14px;
border-left: 1px solid #ffffff10; white-space: nowrap;
padding-left: 8px; overflow: hidden;
text-overflow: ellipsis;
}
.uploadCandidate .filesize {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.uploadCandidate .actions {
display: flex;
align-items: center;
gap: 8px;
}
.remove-button {
width: 32px;
height: 32px;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: ${cssManager.bdTheme('#666', '#999')};
}
.remove-button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
}
.clear-all-button {
margin-bottom: 8px;
text-align: right;
}
.clear-all-button button {
background: none;
border: none;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.clear-all-button button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
}
.validation-message {
font-size: 12px;
margin-top: 4px;
color: #e74c3c;
}
.drop-hint {
text-align: center;
padding: 40px 20px;
color: ${cssManager.bdTheme('#999', '#666')};
font-size: 14px;
}
.drop-hint dees-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.3;
}
.image-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 4px;
line-height: 1.4;
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const hasFiles = this.value.length > 0;
const showClearAll = hasFiles && this.value.length > 1;
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label> ${this.label ? html`
<dees-label .label=${this.label}></dees-label>
` : ''}
<div class="hidden"> <div class="hidden">
<input type="file"> <input
type="file"
?multiple=${this.multiple}
accept="${this.accept}"
>
</div> </div>
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}"> <div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
${this.value.map( ${hasFiles ? html`
(fileArg) => html` ${showClearAll ? html`
<div class="uploadCandidate" @contextmenu=${eventArg => { <div class="clear-all-button">
DeesContextmenu.openContextMenuWithOptions(eventArg, [{ <button @click=${this.clearAll}>Clear All</button>
iconName: 'trash', </div>
name: 'Remove', ` : ''}
action: async () => { <div class="files-container">
this.value.splice(this.value.indexOf(fileArg), 1); ${this.value.map((fileArg) => {
this.requestUpdate(); const fileType = this.getFileType(fileArg);
} const isImage = fileType === 'image';
}]); return html`
}}> <div class="uploadCandidate ${fileType}-file">
<div class="icon"> <div class="icon">
<dees-icon .iconFA=${'paperclip'}></dees-icon> ${isImage && this.canShowPreview(fileArg) ? html`
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
` : html`
<dees-icon .iconName=${this.getFileIcon(fileArg)}></dees-icon>
`}
</div>
<div class="info">
<div class="filename" title="${fileArg.name}">${fileArg.name}</div>
<div class="filesize">${this.formatFileSize(fileArg.size)}</div>
</div>
<div class="actions">
<button
class="remove-button"
@click=${() => this.removeFile(fileArg)}
title="Remove file"
>
<dees-icon .iconName=${'lucide:x'}></dees-icon>
</button>
</div>
</div>
`;
})}
</div> </div>
<div class="description"> ` : html`
<span style="font-weight: 600">${fileArg.name}</span><br /> <div class="drop-hint">
<span style="font-weight: 400">${fileArg.size}</span> <dees-icon .iconName=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div>
</div> </div>
</div> ` `}
)} <div class="uploadButton" @click=${this.openFileSelector}>
<div class="uploadButton" @click=${ <dees-icon .iconName=${'lucide:upload'}></dees-icon>
this.openFileSelector ${this.buttonText}
}> </div>
${this.buttonText}
</div> </div>
</div> ${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
${this.validationState === 'invalid' && this.validationMessage ? html`
<div class="validation-message">${this.validationMessage}</div>
` : ''}
</div> </div>
`; `;
} }
private validationMessage: string = '';
// Utility methods
private formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
private getFileType(file: File): string {
const type = file.type.toLowerCase();
if (type.startsWith('image/')) return 'image';
if (type === 'application/pdf') return 'pdf';
if (type.includes('word') || type.includes('document')) return 'doc';
if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet';
if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation';
if (type.startsWith('video/')) return 'video';
if (type.startsWith('audio/')) return 'audio';
if (type.includes('zip') || type.includes('compressed')) return 'archive';
return 'file';
}
private getFileIcon(file: File): string {
const type = this.getFileType(file);
const iconMap = {
'image': 'lucide:image',
'pdf': 'lucide:file-text',
'doc': 'lucide:file-text',
'spreadsheet': 'lucide:table',
'presentation': 'lucide:presentation',
'video': 'lucide:video',
'audio': 'lucide:music',
'archive': 'lucide:archive',
'file': 'lucide:file'
};
return iconMap[type] || 'lucide:file';
}
private canShowPreview(file: File): boolean {
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
}
private validateFile(file: File): boolean {
// Check file size
if (this.maxSize > 0 && file.size > this.maxSize) {
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
this.validationState = 'invalid';
return false;
}
// Check file type
if (this.accept) {
const acceptedTypes = this.accept.split(',').map(s => s.trim());
let isAccepted = false;
for (const acceptType of acceptedTypes) {
if (acceptType.startsWith('.')) {
// Extension check
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
isAccepted = true;
break;
}
} else if (acceptType.endsWith('/*')) {
// MIME type wildcard check
const mimePrefix = acceptType.slice(0, -2);
if (file.type.startsWith(mimePrefix)) {
isAccepted = true;
break;
}
} else if (file.type === acceptType) {
// Exact MIME type check
isAccepted = true;
break;
}
}
if (!isAccepted) {
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
this.validationState = 'invalid';
return false;
}
}
return true;
}
public async openFileSelector() { public async openFileSelector() {
if (this.disabled) return;
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.click(); inputFile.click();
this.state = 'idle'; }
this.buttonText = 'Upload more files...';
private removeFile(file: File) {
const index = this.value.indexOf(file);
if (index > -1) {
this.value.splice(index, 1);
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
}
}
private clearAll() {
this.value = [];
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
} }
public async updateValue(eventArg: Event) { public async updateValue(eventArg: Event) {
@ -198,52 +512,131 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) { public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.addEventListener('change', (event: Event) => { inputFile.addEventListener('change', async (event: Event) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
for (const file of Array.from(target.files)) { const newFiles = Array.from(target.files);
this.value.push(file); await this.addFiles(newFiles);
}
this.requestUpdate();
console.log(`Got ${this.value.length} files!`);
// Reset the input value to allow selecting the same file again if needed // Reset the input value to allow selecting the same file again if needed
target.value = ''; target.value = '';
}); });
// lets handle drag and drop // Handle drag and drop
const dropArea = this.shadowRoot.querySelector('.maincontainer'); const dropArea = this.shadowRoot.querySelector('.maincontainer');
const handlerFunction = (eventArg: DragEvent) => { const handlerFunction = async (eventArg: DragEvent) => {
eventArg.preventDefault(); eventArg.preventDefault();
eventArg.stopPropagation();
switch (eventArg.type) { switch (eventArg.type) {
case 'dragenter':
case 'dragover': case 'dragover':
this.state = 'dragOver'; this.state = 'dragOver';
this.buttonText = 'release to upload file...';
break; break;
case 'dragleave': case 'dragleave':
this.state = 'idle'; // Check if we're actually leaving the drop area
this.buttonText = 'Upload File...'; const rect = dropArea.getBoundingClientRect();
const x = eventArg.clientX;
const y = eventArg.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
this.state = 'idle';
}
break; break;
case 'drop': case 'drop':
this.state = 'idle'; this.state = 'idle';
this.buttonText = 'Upload more files...'; const files = Array.from(eventArg.dataTransfer.files);
await this.addFiles(files);
break;
} }
console.log(eventArg);
for (const file of Array.from(eventArg.dataTransfer.files)) {
this.value.push(file);
this.requestUpdate();
}
console.log(`Got ${this.value.length} files!`);
}; };
dropArea.addEventListener('dragenter', handlerFunction, false); dropArea.addEventListener('dragenter', handlerFunction, false);
dropArea.addEventListener('dragleave', handlerFunction, false); dropArea.addEventListener('dragleave', handlerFunction, false);
dropArea.addEventListener('dragover', handlerFunction, false); dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false); dropArea.addEventListener('drop', handlerFunction, false);
} }
private async addFiles(files: File[]) {
const filesToAdd: File[] = [];
for (const file of files) {
if (this.validateFile(file)) {
filesToAdd.push(file);
}
}
if (filesToAdd.length === 0) return;
// Check max files limit
if (this.maxFiles > 0) {
const totalFiles = this.value.length + filesToAdd.length;
if (totalFiles > this.maxFiles) {
const allowedCount = this.maxFiles - this.value.length;
if (allowedCount <= 0) {
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
this.validationState = 'invalid';
return;
}
filesToAdd.splice(allowedCount);
this.validationMessage = `Only ${allowedCount} more file(s) can be added`;
this.validationState = 'warn';
}
}
// Add files
if (!this.multiple && filesToAdd.length > 0) {
this.value = [filesToAdd[0]];
} else {
this.value.push(...filesToAdd);
}
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
// Update button text
if (this.value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
}
}
public async validate(): Promise<boolean> {
this.validationMessage = '';
if (this.required && this.value.length === 0) {
this.validationState = 'invalid';
this.validationMessage = 'Please select at least one file';
return false;
}
// Validate all files
for (const file of this.value) {
if (!this.validateFile(file)) {
return false;
}
}
this.validationState = 'valid';
return true;
}
public getValue(): File[] { public getValue(): File[] {
return this.value; return this.value;
} }
public setValue(value: File[]): void { public setValue(value: File[]): void {
this.value = value; this.value = value;
this.requestUpdate();
if (value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Upload File...';
}
}
public updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.validate();
}
} }
} }

View File

@ -1,267 +0,0 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import type { DeesInputRadio } from './dees-input-radio.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 16px;
}
@media (prefers-color-scheme: dark) {
.radio-group {
background: #0a0a0a;
}
}
.radio-group-title {
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
@media (prefers-color-scheme: dark) {
.radio-group-title {
color: #ccc;
}
}
.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Radio Groups</h3>
<p>Radio buttons for single-choice selections</p>
<div class="radio-group">
<div class="radio-group-title">Select your subscription plan:</div>
<dees-input-radio
.label=${'Basic Plan - $9/month'}
.value=${true}
.key=${'plan-basic'}
.name=${'plan'}
></dees-input-radio>
<dees-input-radio
.label=${'Pro Plan - $29/month'}
.key=${'plan-pro'}
.name=${'plan'}
></dees-input-radio>
<dees-input-radio
.label=${'Enterprise Plan - $99/month'}
.key=${'plan-enterprise'}
.name=${'plan'}
></dees-input-radio>
</div>
<div class="radio-group">
<div class="radio-group-title">Task Priority:</div>
<dees-input-radio
.label=${'High Priority'}
.key=${'priority-high'}
.name=${'priority'}
></dees-input-radio>
<dees-input-radio
.label=${'Medium Priority'}
.value=${true}
.key=${'priority-medium'}
.name=${'priority'}
></dees-input-radio>
<dees-input-radio
.label=${'Low Priority'}
.key=${'priority-low'}
.name=${'priority'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Radio buttons arranged horizontally for yes/no questions</p>
<div class="radio-group" style="flex-direction: row;">
<div style="margin-right: 16px;">Do you agree?</div>
<dees-input-radio
.label=${'Yes'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'agree-yes'}
.name=${'agreement'}
></dees-input-radio>
<dees-input-radio
.label=${'No'}
.layoutMode=${'horizontal'}
.key=${'agree-no'}
.name=${'agreement'}
></dees-input-radio>
<dees-input-radio
.label=${'Maybe'}
.layoutMode=${'horizontal'}
.key=${'agree-maybe'}
.name=${'agreement'}
></dees-input-radio>
</div>
<div class="radio-group" style="flex-direction: row;">
<div style="margin-right: 16px;">Experience Level:</div>
<dees-input-radio
.label=${'Beginner'}
.layoutMode=${'horizontal'}
.key=${'exp-beginner'}
.name=${'experience'}
></dees-input-radio>
<dees-input-radio
.label=${'Intermediate'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'exp-intermediate'}
.name=${'experience'}
></dees-input-radio>
<dees-input-radio
.label=${'Expert'}
.layoutMode=${'horizontal'}
.key=${'exp-expert'}
.name=${'experience'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Survey Example</h3>
<p>Multiple radio groups in a survey format</p>
<div class="grid-layout">
<div class="radio-group">
<div class="radio-group-title">How satisfied are you?</div>
<dees-input-radio .label=${'Very Satisfied'} .key=${'sat-very'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Satisfied'} .value=${true} .key=${'sat-normal'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Neutral'} .key=${'sat-neutral'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Dissatisfied'} .key=${'sat-dis'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Very Dissatisfied'} .key=${'sat-verydis'} .name=${'satisfaction'}></dees-input-radio>
</div>
<div class="radio-group">
<div class="radio-group-title">Would you recommend us?</div>
<dees-input-radio .label=${'Definitely'} .key=${'rec-def'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Probably'} .value=${true} .key=${'rec-prob'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Not Sure'} .key=${'rec-unsure'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Probably Not'} .key=${'rec-probnot'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Definitely Not'} .key=${'rec-defnot'} .name=${'recommend'}></dees-input-radio>
</div>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<p>Different radio button states</p>
<div class="radio-group">
<dees-input-radio
.label=${'Normal Radio'}
.key=${'state-normal'}
.name=${'states'}
></dees-input-radio>
<dees-input-radio
.label=${'Selected Radio'}
.value=${true}
.key=${'state-selected'}
.name=${'states'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Unchecked'}
.disabled=${true}
.key=${'state-disabled1'}
.name=${'states2'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Checked'}
.disabled=${true}
.value=${true}
.key=${'state-disabled2'}
.name=${'states2'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Settings Example</h3>
<p>Common radio button patterns in settings</p>
<div class="radio-group">
<div class="radio-group-title">Theme Preference:</div>
<dees-input-radio .label=${'Light Theme'} .key=${'theme-light'} .name=${'theme'}></dees-input-radio>
<dees-input-radio .label=${'Dark Theme'} .value=${true} .key=${'theme-dark'} .name=${'theme'}></dees-input-radio>
<dees-input-radio .label=${'System Default'} .key=${'theme-system'} .name=${'theme'}></dees-input-radio>
</div>
<div class="radio-group">
<div class="radio-group-title">Notification Frequency:</div>
<dees-input-radio .label=${'All Notifications'} .key=${'notif-all'} .name=${'notifications'}></dees-input-radio>
<dees-input-radio .label=${'Important Only'} .value=${true} .key=${'notif-important'} .name=${'notifications'}></dees-input-radio>
<dees-input-radio .label=${'None'} .key=${'notif-none'} .name=${'notifications'}></dees-input-radio>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -1,135 +0,0 @@
import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radio.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-radio': DeesInputRadio;
}
}
@customElement('dees-input-radio')
export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
public static demo = demoFunc;
// INSTANCE
@property()
public value: boolean = false;
@property({ type: String })
public name: string = '';
constructor() {
super();
this.labelPosition = 'right'; // Radio buttons default to label on the right
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
position: relative;
}
.maincontainer {
transition: all 0.3s;
padding: 5px 0px;
color: #ccc;
}
.maincontainer:hover {
color: #fff;
}
input:focus {
outline: none;
border-bottom: 1px solid #e4002b;
}
.checkbox {
transition: all 0.3s;
box-sizing: border-box;
border-radius: 20px;
border: 1px solid #999;
height: 24px;
width: 24px;
display: inline-block;
background: #222;
}
.checkbox.selected {
background: #0050b9;
border: 1px solid #0050b9;
}
.maincontainer:hover .checkbox.selected {
background: #03A9F4;
}
.innercircle {
transition: all 0.3s;
margin: 6px 0px 0px 6px;
background: #222;
width: 10px;
height: 10px;
border-radius: 10px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
</div>
</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
public async toggleSelected () {
// Radio buttons can only be selected, not deselected by clicking
if (this.value) {
return;
}
// If this radio has a name, find and deselect other radios in the same group
if (this.name) {
// Try to find a form container first, then fall back to document
const container = this.closest('dees-form') ||
this.closest('dees-demowrapper') ||
this.closest('.radio-group')?.parentElement ||
document;
const allRadios = container.querySelectorAll(`dees-input-radio[name="${this.name}"]`);
allRadios.forEach((radio: DeesInputRadio) => {
if (radio !== this && radio.value) {
radio.value = false;
}
});
}
this.value = true;
this.dispatchEvent(new CustomEvent('newValue', {
detail: this.value,
bubbles: true
}));
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
}

View File

@ -0,0 +1,200 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.result-display {
margin-top: 16px;
padding: 12px;
background: rgba(0, 105, 242, 0.1);
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Radio Groups'} .subtitle=${'Simple string options for common use cases'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'Subscription Plan'}
.options=${['Basic - $9/month', 'Pro - $29/month', 'Enterprise - $99/month']}
.selectedOption=${'Pro - $29/month'}
.description=${'Choose your subscription tier'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Priority Level'}
.options=${['High', 'Medium', 'Low']}
.selectedOption=${'Medium'}
.required=${true}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
<dees-input-radiogroup
.label=${'Do you agree with the terms?'}
.options=${['Yes', 'No', 'Maybe']}
.direction=${'horizontal'}
.selectedOption=${'Yes'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Experience Level'}
.options=${['Beginner', 'Intermediate', 'Expert']}
.direction=${'horizontal'}
.selectedOption=${'Intermediate'}
.description=${'Select your experience level with web development'}
></dees-input-radiogroup>
</dees-panel>
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}>
<dees-input-radiogroup
id="advanced-radio"
.label=${'Select Region'}
.options=${[
{ option: 'United States (US East)', key: 'us-east', payload: { region: 'us-east-1', latency: 20 } },
{ option: 'Europe (Frankfurt)', key: 'eu-central', payload: { region: 'eu-central-1', latency: 50 } },
{ option: 'Asia Pacific (Singapore)', key: 'ap-southeast', payload: { region: 'ap-southeast-1', latency: 120 } }
]}
.selectedOption=${'eu-central'}
.description=${'Choose the closest region for optimal performance'}
@change=${(e: CustomEvent) => {
const display = document.querySelector('#region-result');
if (display) {
display.textContent = 'Selected: ' + JSON.stringify(e.detail.value, null, 2);
}
}}
></dees-input-radiogroup>
<div id="region-result" class="result-display">Selected: { "region": "eu-central-1", "latency": 50 }</div>
</dees-panel>
<dees-panel .title=${'4. Survey Example'} .subtitle=${'Multiple radio groups for surveys and forms'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'How satisfied are you?'}
.options=${['Very Satisfied', 'Satisfied', 'Neutral', 'Dissatisfied', 'Very Dissatisfied']}
.selectedOption=${'Satisfied'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Would you recommend us?'}
.options=${['Definitely', 'Probably', 'Not Sure', 'Probably Not', 'Definitely Not']}
.selectedOption=${'Probably'}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'5. States & Validation'} .subtitle=${'Different states and validation examples'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'Required Selection'}
.options=${['Option A', 'Option B', 'Option C']}
.required=${true}
.description=${'This field is required'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Disabled State'}
.options=${['Disabled Option 1', 'Disabled Option 2', 'Disabled Option 3']}
.selectedOption=${'Disabled Option 2'}
.disabled=${true}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
<dees-input-radiogroup
.label=${'Theme Preference'}
.options=${[
{ option: 'Light Theme', key: 'light', payload: 'light' },
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
{ option: 'System Default', key: 'system', payload: 'auto' }
]}
.selectedOption=${'dark'}
.description=${'Choose how the application should appear'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Notification Frequency'}
.options=${['All Notifications', 'Important Only', 'None']}
.selectedOption=${'Important Only'}
.description=${'Control how often you receive notifications'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Language'}
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
.selectedOption=${'English'}
.direction=${'horizontal'}
></dees-input-radiogroup>
</dees-panel>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>
<dees-form>
<dees-input-text
.label=${'Product Name'}
.required=${true}
.key=${'productName'}
></dees-input-text>
<dees-input-radiogroup
.label=${'Product Category'}
.options=${['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports']}
.required=${true}
.key=${'category'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Condition'}
.options=${['New', 'Like New', 'Good', 'Fair', 'Poor']}
.direction=${'horizontal'}
.key=${'condition'}
.selectedOption=${'New'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Shipping Speed'}
.options=${[
{ option: 'Standard (5-7 days)', key: 'standard', payload: { days: 7, price: 0 } },
{ option: 'Express (2-3 days)', key: 'express', payload: { days: 3, price: 10 } },
{ option: 'Overnight', key: 'overnight', payload: { days: 1, price: 25 } }
]}
.selectedOption=${'standard'}
.key=${'shipping'}
></dees-input-radiogroup>
<dees-form-submit .text=${'Submit Product'}></dees-form-submit>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,295 @@
import {
customElement,
type TemplateResult,
property,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radiogroup.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-radiogroup': DeesInputRadiogroup;
}
}
type RadioOption = string | { option: string; key: string; payload?: any };
@customElement('dees-input-radiogroup')
export class DeesInputRadiogroup extends DeesInputBase<string | object> {
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public options: RadioOption[] = [];
@property()
public selectedOption: string = '';
@property({ type: String })
public direction: 'vertical' | 'horizontal' = 'vertical';
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
// Form compatibility
public get value() {
const option = this.getOptionByKey(this.selectedOption);
if (typeof option === 'object' && option.payload !== undefined) {
return option.payload;
}
return this.selectedOption;
}
public set value(val: string | any) {
if (typeof val === 'string') {
this.selectedOption = val;
} else {
// Try to find option by payload
const option = this.options.find(opt =>
typeof opt === 'object' && opt.payload === val
);
if (option && typeof option === 'object') {
this.selectedOption = option.key;
}
}
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
}
.maincontainer {
display: flex;
flex-direction: column;
gap: 8px;
}
.maincontainer.horizontal {
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
}
.radio-option {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
position: relative;
}
.maincontainer.horizontal .radio-option {
padding: 8px 16px 8px 0;
}
.radio-option:hover .radio-circle {
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')};
}
.radio-option:hover .radio-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.radio-circle {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('#999', '#666')};
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
transition: all 0.2s ease;
position: relative;
flex-shrink: 0;
}
.radio-option.selected .radio-circle {
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')};
background: ${cssManager.bdTheme('#0050b9', '#0084ff')};
}
.radio-option.selected .radio-circle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
}
.radio-label {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
transition: color 0.2s ease;
}
.radio-option.selected .radio-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
font-weight: 500;
}
:host([disabled]) .radio-option {
cursor: not-allowed;
opacity: 0.5;
}
:host([disabled]) .radio-option:hover .radio-circle {
border-color: ${cssManager.bdTheme('#999', '#666')};
}
:host([disabled]) .radio-option:hover .radio-label {
color: ${cssManager.bdTheme('#666', '#999')};
}
.label-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#333', '#ccc')};
margin-bottom: 8px;
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 8px;
line-height: 1.4;
}
/* Validation styles */
:host([validationState="invalid"]) .radio-circle {
border-color: #e74c3c;
}
:host([validationState="valid"]) .radio-option.selected .radio-circle {
border-color: #27ae60;
background: #27ae60;
}
:host([validationState="warn"]) .radio-option.selected .radio-circle {
border-color: #f39c12;
background: #f39c12;
}
/* Override base grid layout for radiogroup to prevent large gaps */
:host([label-position="left"]) .input-wrapper {
grid-template-columns: auto auto;
}
:host([label-position="right"]) .input-wrapper {
grid-template-columns: auto auto;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<div class="label-text">${this.label}</div>` : ''}
<div class="maincontainer ${this.direction}">
${this.options.map((option) => {
const optionKey = this.getOptionKey(option);
const optionLabel = this.getOptionLabel(option);
const isSelected = this.selectedOption === optionKey;
return html`
<div
class="radio-option ${isSelected ? 'selected' : ''}"
@click="${() => this.selectOption(optionKey)}"
>
<div class="radio-circle"></div>
<div class="radio-label">${optionLabel}</div>
</div>
`;
})}
</div>
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
</div>
`;
}
private getOptionKey(option: RadioOption): string {
if (typeof option === 'string') {
return option;
}
return option.key;
}
private getOptionLabel(option: RadioOption): string {
if (typeof option === 'string') {
return option;
}
return option.option;
}
private getOptionByKey(key: string): RadioOption | undefined {
return this.options.find(opt => this.getOptionKey(opt) === key);
}
private selectOption(key: string): void {
if (this.disabled) {
return;
}
const oldValue = this.selectedOption;
this.selectedOption = key;
if (oldValue !== key) {
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
this.dispatchEvent(new CustomEvent('input', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
this.changeSubject.next(this);
}
}
public getValue(): string | any {
return this.value;
}
public setValue(val: string | any): void {
this.value = val;
}
public async validate(): Promise<boolean> {
if (this.required && !this.selectedOption) {
this.validationState = 'invalid';
return false;
}
this.validationState = 'valid';
return true;
}
public async firstUpdated() {
// Auto-select first option if none selected and not required
if (!this.selectedOption && this.options.length > 0 && !this.required) {
const firstOption = this.options[0];
this.selectedOption = this.getOptionKey(firstOption);
}
}
}

View File

@ -0,0 +1,133 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.output-preview {
margin-top: 16px;
padding: 16px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.output-preview {
background: #2c2c2c;
color: #e4e4e7;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Rich Text Editor'} .subtitle=${'A full-featured rich text editor with formatting toolbar'}>
<dees-input-richtext
.label=${'Article Content'}
.value=${'<h1>Welcome to the Rich Text Editor!</h1><p>This is a feature-rich editor built with TipTap. You can:</p><ul><li><strong>Format text</strong> with <em>various</em> <u>styles</u></li><li>Create different heading levels</li><li>Add <a href="https://example.com">links</a> to external resources</li><li>Write <code>inline code</code> or code blocks</li></ul><blockquote><p>Use the toolbar above to explore all the formatting options available!</p></blockquote><p>Start typing to see the magic happen...</p>'}
.description=${'Use the toolbar to format your content with headings, lists, links, and more'}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'2. With Placeholder'} .subtitle=${'Empty editor with placeholder text'}>
<dees-input-richtext
.label=${'Blog Post'}
.placeholder=${'Start writing your blog post here...'}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'3. Different Heights'} .subtitle=${'Editors with different minimum heights for various use cases'}>
<div class="grid-layout">
<dees-input-richtext
.label=${'Short Note'}
.minHeight=${150}
.placeholder=${'Quick note...'}
.showWordCount=${false}
></dees-input-richtext>
<dees-input-richtext
.label=${'Extended Content'}
.minHeight=${300}
.placeholder=${'Write your extended content here...'}
.showWordCount=${true}
></dees-input-richtext>
</div>
</dees-panel>
<dees-panel .title=${'4. Code Examples'} .subtitle=${'Editor pre-filled with code examples'}>
<dees-input-richtext
.label=${'Technical Documentation'}
.value=${'<h2>Installation Guide</h2><p>To install the package, run the following command:</p><pre><code>npm install @design.estate/dees-catalog</code></pre><p>Then import the component in your TypeScript file:</p><pre><code>import { DeesInputRichtext } from "@design.estate/dees-catalog";</code></pre><p>You can now use the <code>&lt;dees-input-richtext&gt;</code> element in your templates.</p>'}
.minHeight=${250}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only rich text content'}>
<dees-input-richtext
.label=${'Published Article (Read Only)'}
.value=${'<h2>The Future of Web Components</h2><p>Web Components have revolutionized how we build modern web applications...</p><blockquote><p>"The future of web development lies in reusable, encapsulated components."</p></blockquote>'}
.disabled=${true}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'6. Interactive Demo'} .subtitle=${'Type in the editor below and see the HTML output'}>
<dees-input-richtext
id="interactive-editor"
.label=${'Try it yourself'}
.placeholder=${'Type something here...'}
.showWordCount=${true}
@change=${(e: CustomEvent) => {
const output = document.querySelector('#output-preview');
if (output) {
output.textContent = e.detail.value;
}
}}
></dees-input-richtext>
<div class="output-preview" id="output-preview">
<em>HTML output will appear here...</em>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,710 @@
import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-richtext.demo.js';
import './dees-icon.js';
import {
customElement,
type TemplateResult,
property,
html,
css,
cssManager,
state,
query,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Typography from '@tiptap/extension-typography';
declare global {
interface HTMLElementTagNameMap {
'dees-input-richtext': DeesInputRichtext;
}
}
interface IToolbarButton {
name: string;
icon?: string;
action?: () => void;
isActive?: () => boolean;
title: string;
isDivider?: boolean;
}
@customElement('dees-input-richtext')
export class DeesInputRichtext extends DeesInputBase<string> {
public static demo = demoFunc;
// INSTANCE
@property({
type: String,
reflect: true,
})
public value: string = '';
@property({
type: String,
})
public placeholder: string = '';
@property({
type: Boolean,
})
public showWordCount: boolean = true;
@property({
type: Number,
})
public minHeight: number = 200;
@state()
private showLinkInput: boolean = false;
@state()
private wordCount: number = 0;
@query('.editor-content')
private editorElement: HTMLElement;
@query('.link-input input')
private linkInputElement: HTMLInputElement;
private editor: Editor;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.input-wrapper {
position: relative;
}
.label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
}
.editor-container {
display: flex;
flex-direction: column;
min-height: ${cssManager.bdTheme('200px', '200px')};
border: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
border-radius: 8px;
background: ${cssManager.bdTheme('#ffffff', '#141414')};
overflow: hidden;
transition: all 0.2s ease;
}
.editor-container:hover {
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
}
.editor-container.focused {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
border-bottom: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
align-items: center;
position: relative;
}
.toolbar-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
background: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#9ca3af')};
transition: all 0.2s;
user-select: none;
}
.toolbar-button dees-icon {
width: 16px;
height: 16px;
}
.toolbar-button:hover {
background: ${cssManager.bdTheme('#e5e7eb', '#2c2c2c')};
color: ${cssManager.bdTheme('#1f2937', '#e4e4e7')};
}
.toolbar-button.active {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: white;
}
.toolbar-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: ${cssManager.bdTheme('#d1d5db', '#404040')};
margin: 0 4px;
}
.editor-content {
flex: 1;
padding: 16px;
overflow-y: auto;
min-height: var(--min-height, 200px);
}
.editor-content .ProseMirror {
outline: none;
line-height: 1.6;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
min-height: 100%;
}
.editor-content .ProseMirror p {
margin: 0.5em 0;
}
.editor-content .ProseMirror p:first-child {
margin-top: 0;
}
.editor-content .ProseMirror p:last-child {
margin-bottom: 0;
}
.editor-content .ProseMirror h1 {
font-size: 2em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.2;
}
.editor-content .ProseMirror h2 {
font-size: 1.5em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.3;
}
.editor-content .ProseMirror h3 {
font-size: 1.25em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.4;
}
.editor-content .ProseMirror ul,
.editor-content .ProseMirror ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.editor-content .ProseMirror li {
margin: 0.25em 0;
}
.editor-content .ProseMirror blockquote {
border-left: 4px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
margin: 1em 0;
padding-left: 1em;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-style: italic;
}
.editor-content .ProseMirror code {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
border-radius: 4px;
padding: 0.2em 0.4em;
font-family: 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em;
color: ${cssManager.bdTheme('#e11d48', '#f87171')};
}
.editor-content .ProseMirror pre {
background: ${cssManager.bdTheme('#1f2937', '#0a0a0a')};
color: ${cssManager.bdTheme('#f9fafb', '#e4e4e7')};
border-radius: 6px;
padding: 1em;
margin: 1em 0;
overflow-x: auto;
}
.editor-content .ProseMirror pre code {
background: none;
color: inherit;
padding: 0;
border-radius: 0;
}
.editor-content .ProseMirror a {
color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
text-decoration: underline;
cursor: pointer;
}
.editor-content .ProseMirror a:hover {
color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
}
.editor-footer {
padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
border-top: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
display: flex;
justify-content: space-between;
align-items: center;
}
.word-count {
font-weight: 500;
}
.link-input {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 12px;
z-index: 1000;
}
.link-input.show {
display: block;
}
.link-input input {
width: 100%;
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 4px;
outline: none;
font-size: 14px;
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
}
.link-input input:focus {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
}
.link-input-buttons {
display: flex;
gap: 8px;
margin-top: 8px;
}
.link-input-buttons button {
padding: 6px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 4px;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
cursor: pointer;
font-size: 12px;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
transition: all 0.2s;
}
.link-input-buttons button:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
}
.link-input-buttons button.primary {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: white;
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
}
.link-input-buttons button.primary:hover {
background: ${cssManager.bdTheme('#0069f2', '#0084ff')};
}
.description {
margin-top: 8px;
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
line-height: 1.4;
}
:host([disabled]) .editor-container {
opacity: 0.6;
cursor: not-allowed;
}
:host([disabled]) .toolbar-button,
:host([disabled]) .editor-content {
pointer-events: none;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<label class="label">${this.label}</label>` : ''}
<div class="editor-container ${this.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${this.minHeight}px">
<div class="editor-toolbar">
${this.renderToolbar()}
<div class="link-input ${this.showLinkInput ? 'show' : ''}">
<input type="url" placeholder="Enter URL..." @keydown=${this.handleLinkInputKeydown} />
<div class="link-input-buttons">
<button class="primary" @click=${this.saveLink}>Save</button>
<button @click=${this.removeLink}>Remove</button>
<button @click=${this.hideLinkInput}>Cancel</button>
</div>
</div>
</div>
<div class="editor-content"></div>
${this.showWordCount
? html`
<div class="editor-footer">
<span class="word-count">${this.wordCount} word${this.wordCount !== 1 ? 's' : ''}</span>
</div>
`
: ''}
</div>
${this.description ? html`<div class="description">${this.description}</div>` : ''}
</div>
`;
}
private renderToolbar(): TemplateResult {
const buttons: IToolbarButton[] = this.getToolbarButtons();
return html`
${buttons.map((button) => {
if (button.isDivider) {
return html`<div class="toolbar-divider"></div>`;
}
return html`
<button
class="toolbar-button ${button.isActive?.() ? 'active' : ''}"
@click=${button.action}
title=${button.title}
?disabled=${this.disabled || !this.editor}
>
<dees-icon .icon=${button.icon}></dees-icon>
</button>
`;
})}
`;
}
private getToolbarButtons(): IToolbarButton[] {
if (!this.editor) return [];
return [
{
name: 'bold',
icon: 'lucide:bold',
title: 'Bold (Ctrl+B)',
action: () => this.editor.chain().focus().toggleBold().run(),
isActive: () => this.editor.isActive('bold'),
},
{
name: 'italic',
icon: 'lucide:italic',
title: 'Italic (Ctrl+I)',
action: () => this.editor.chain().focus().toggleItalic().run(),
isActive: () => this.editor.isActive('italic'),
},
{
name: 'underline',
icon: 'lucide:underline',
title: 'Underline (Ctrl+U)',
action: () => this.editor.chain().focus().toggleUnderline().run(),
isActive: () => this.editor.isActive('underline'),
},
{
name: 'strike',
icon: 'lucide:strikethrough',
title: 'Strikethrough',
action: () => this.editor.chain().focus().toggleStrike().run(),
isActive: () => this.editor.isActive('strike'),
},
{ name: 'divider1', title: '', isDivider: true },
{
name: 'h1',
icon: 'lucide:heading1',
title: 'Heading 1',
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => this.editor.isActive('heading', { level: 1 }),
},
{
name: 'h2',
icon: 'lucide:heading2',
title: 'Heading 2',
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => this.editor.isActive('heading', { level: 2 }),
},
{
name: 'h3',
icon: 'lucide:heading3',
title: 'Heading 3',
action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => this.editor.isActive('heading', { level: 3 }),
},
{ name: 'divider2', title: '', isDivider: true },
{
name: 'bulletList',
icon: 'lucide:list',
title: 'Bullet List',
action: () => this.editor.chain().focus().toggleBulletList().run(),
isActive: () => this.editor.isActive('bulletList'),
},
{
name: 'orderedList',
icon: 'lucide:listOrdered',
title: 'Numbered List',
action: () => this.editor.chain().focus().toggleOrderedList().run(),
isActive: () => this.editor.isActive('orderedList'),
},
{
name: 'blockquote',
icon: 'lucide:quote',
title: 'Quote',
action: () => this.editor.chain().focus().toggleBlockquote().run(),
isActive: () => this.editor.isActive('blockquote'),
},
{
name: 'code',
icon: 'lucide:code',
title: 'Code',
action: () => this.editor.chain().focus().toggleCode().run(),
isActive: () => this.editor.isActive('code'),
},
{
name: 'codeBlock',
icon: 'lucide:fileCode',
title: 'Code Block',
action: () => this.editor.chain().focus().toggleCodeBlock().run(),
isActive: () => this.editor.isActive('codeBlock'),
},
{ name: 'divider3', title: '', isDivider: true },
{
name: 'link',
icon: 'lucide:link',
title: 'Add Link',
action: () => this.toggleLink(),
isActive: () => this.editor.isActive('link'),
},
{
name: 'alignLeft',
icon: 'lucide:alignLeft',
title: 'Align Left',
action: () => this.editor.chain().focus().setTextAlign('left').run(),
isActive: () => this.editor.isActive({ textAlign: 'left' }),
},
{
name: 'alignCenter',
icon: 'lucide:alignCenter',
title: 'Align Center',
action: () => this.editor.chain().focus().setTextAlign('center').run(),
isActive: () => this.editor.isActive({ textAlign: 'center' }),
},
{
name: 'alignRight',
icon: 'lucide:alignRight',
title: 'Align Right',
action: () => this.editor.chain().focus().setTextAlign('right').run(),
isActive: () => this.editor.isActive({ textAlign: 'right' }),
},
{ name: 'divider4', title: '', isDivider: true },
{
name: 'undo',
icon: 'lucide:undo',
title: 'Undo (Ctrl+Z)',
action: () => this.editor.chain().focus().undo().run(),
},
{
name: 'redo',
icon: 'lucide:redo',
title: 'Redo (Ctrl+Y)',
action: () => this.editor.chain().focus().redo().run(),
},
];
}
public async firstUpdated() {
await this.updateComplete;
this.initializeEditor();
}
private initializeEditor(): void {
if (this.disabled) return;
this.editor = new Editor({
element: this.editorElement,
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
Underline,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'editor-link',
},
}),
Typography,
],
content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''),
onUpdate: ({ editor }) => {
this.value = editor.getHTML();
this.updateWordCount();
this.dispatchEvent(
new CustomEvent('input', {
detail: { value: this.value },
bubbles: true,
composed: true,
})
);
this.dispatchEvent(
new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true,
})
);
},
onSelectionUpdate: () => {
this.requestUpdate();
},
onFocus: () => {
this.requestUpdate();
},
onBlur: () => {
this.requestUpdate();
},
});
this.updateWordCount();
}
private updateWordCount(): void {
if (!this.editor) return;
const text = this.editor.getText();
this.wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
}
private toggleLink(): void {
if (!this.editor) return;
if (this.editor.isActive('link')) {
const href = this.editor.getAttributes('link').href;
this.showLinkInput = true;
requestAnimationFrame(() => {
if (this.linkInputElement) {
this.linkInputElement.value = href || '';
this.linkInputElement.focus();
this.linkInputElement.select();
}
});
} else {
this.showLinkInput = true;
requestAnimationFrame(() => {
if (this.linkInputElement) {
this.linkInputElement.value = '';
this.linkInputElement.focus();
}
});
}
}
private saveLink(): void {
if (!this.editor || !this.linkInputElement) return;
const url = this.linkInputElement.value;
if (url) {
this.editor.chain().focus().setLink({ href: url }).run();
}
this.hideLinkInput();
}
private removeLink(): void {
if (!this.editor) return;
this.editor.chain().focus().unsetLink().run();
this.hideLinkInput();
}
private hideLinkInput(): void {
this.showLinkInput = false;
this.editor?.commands.focus();
}
private handleLinkInputKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter') {
e.preventDefault();
this.saveLink();
} else if (e.key === 'Escape') {
e.preventDefault();
this.hideLinkInput();
}
}
public setValue(value: string): void {
this.value = value;
if (this.editor && value !== this.editor.getHTML()) {
this.editor.commands.setContent(value);
}
}
public getValue(): string {
return this.value;
}
public clear(): void {
this.setValue('');
}
public focus(): void {
this.editor?.commands.focus();
}
public async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
if (this.editor) {
this.editor.destroy();
}
}
}

View File

@ -0,0 +1,248 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.output-preview {
margin-top: 16px;
padding: 16px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.output-preview {
background: #2c2c2c;
color: #e4e4e7;
}
}
.tag-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px;
background: #f9fafb;
border-radius: 4px;
min-height: 40px;
align-items: center;
}
@media (prefers-color-scheme: dark) {
.tag-preview {
background: #1f2937;
}
}
.tag-preview-item {
display: inline-block;
padding: 4px 12px;
background: #e0e7ff;
color: #4338ca;
border-radius: 12px;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.tag-preview-item {
background: #312e81;
color: #c7d2fe;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Tags Input'} .subtitle=${'Simple tag input with common programming languages'}>
<dees-input-tags
.label=${'Programming Languages'}
.placeholder=${'Add a language...'}
.value=${['JavaScript', 'TypeScript', 'Python', 'Go']}
.description=${'Press Enter or comma to add tags'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'2. Tags with Suggestions'} .subtitle=${'Auto-complete suggestions for faster input'}>
<dees-input-tags
.label=${'Tech Stack'}
.placeholder=${'Type to see suggestions...'}
.suggestions=${[
'React', 'Vue', 'Angular', 'Svelte', 'Lit', 'Next.js', 'Nuxt', 'SvelteKit',
'Node.js', 'Deno', 'Bun', 'Express', 'Fastify', 'Nest.js', 'Koa',
'MongoDB', 'PostgreSQL', 'Redis', 'MySQL', 'SQLite', 'Cassandra',
'Docker', 'Kubernetes', 'AWS', 'Azure', 'GCP', 'Vercel', 'Netlify'
]}
.value=${['React', 'Node.js', 'PostgreSQL', 'Docker']}
.description=${'Start typing to see suggestions from popular technologies'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'3. Limited Tags'} .subtitle=${'Restrict the number of tags users can add'}>
<div class="grid-layout">
<dees-input-tags
.label=${'Top 3 Skills'}
.placeholder=${'Add up to 3 skills...'}
.maxTags=${3}
.value=${['Design', 'Development']}
.description=${'Maximum 3 tags allowed'}
></dees-input-tags>
<dees-input-tags
.label=${'Categories (Max 5)'}
.placeholder=${'Select categories...'}
.maxTags=${5}
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
.value=${['Tutorial', 'Guide']}
.description=${'Choose up to 5 categories'}
></dees-input-tags>
</div>
</dees-panel>
<dees-panel .title=${'4. Required & Validation'} .subtitle=${'Tags input with validation requirements'}>
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add at least one tag...'}
.required=${true}
.description=${'This field is required - add at least one tag'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only tags display'}>
<dees-input-tags
.label=${'System Tags'}
.value=${['System', 'Protected', 'Read-Only', 'Archive']}
.disabled=${true}
.description=${'These tags cannot be modified'}
></dees-input-tags>
</dees-panel>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Tags input working within a form context'}>
<dees-form>
<dees-input-text
.label=${'Project Name'}
.placeholder=${'My Awesome Project'}
.required=${true}
.key=${'name'}
></dees-input-text>
<div class="grid-layout">
<dees-input-tags
.label=${'Technologies Used'}
.placeholder=${'Add technologies...'}
.required=${true}
.key=${'technologies'}
.suggestions=${[
'TypeScript', 'JavaScript', 'Python', 'Go', 'Rust',
'React', 'Vue', 'Angular', 'Svelte',
'Node.js', 'Deno', 'Express', 'FastAPI'
]}
></dees-input-tags>
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add descriptive tags...'}
.key=${'tags'}
.maxTags=${10}
.suggestions=${[
'frontend', 'backend', 'fullstack', 'mobile', 'desktop',
'web', 'api', 'database', 'devops', 'ui/ux',
'opensource', 'saas', 'enterprise', 'startup'
]}
></dees-input-tags>
</div>
<dees-input-text
.label=${'Description'}
.inputType=${'textarea'}
.placeholder=${'Describe your project...'}
.key=${'description'}
></dees-input-text>
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Add tags and see them collected in real-time'}>
<dees-input-tags
id="interactive-tags"
.label=${'Your Interests'}
.placeholder=${'Type your interests...'}
.suggestions=${[
'Music', 'Movies', 'Books', 'Travel', 'Photography',
'Cooking', 'Gaming', 'Sports', 'Art', 'Technology',
'Fashion', 'Fitness', 'Nature', 'Science', 'History'
]}
@change=${(e: CustomEvent) => {
const preview = document.querySelector('#tags-preview');
const tags = e.detail.value;
if (preview) {
if (tags.length === 0) {
preview.innerHTML = '<em style="color: #999;">No tags added yet...</em>';
} else {
preview.innerHTML = tags.map((tag: string) =>
`<span class="tag-preview-item">${tag}</span>`
).join('');
}
}
}}
></dees-input-tags>
<div class="tag-preview" id="tags-preview">
<em style="color: #999;">No tags added yet...</em>
</div>
<div class="output-preview" id="tags-json">
<em>JSON output will appear here...</em>
</div>
<script>
// Update JSON preview
const tagsInput = document.querySelector('#interactive-tags');
tagsInput?.addEventListener('change', (e) => {
const jsonPreview = document.querySelector('#tags-json');
if (jsonPreview) {
jsonPreview.textContent = JSON.stringify(e.detail.value, null, 2);
}
});
</script>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,401 @@
import {
customElement,
html,
css,
cssManager,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import './dees-icon.js';
import { demoFunc } from './dees-input-tags.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-tags': DeesInputTags;
}
}
@customElement('dees-input-tags')
export class DeesInputTags extends DeesInputBase<DeesInputTags> {
// STATIC
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public value: string[] = [];
@property({ type: String })
public placeholder: string = 'Add tags...';
@property({ type: Number })
public maxTags: number = 0; // 0 means unlimited
@property({ type: Array })
public suggestions: string[] = [];
@state()
private inputValue: string = '';
@state()
private showSuggestions: boolean = false;
@state()
private highlightedSuggestionIndex: number = -1;
@property({ type: String })
public validationText: string = '';
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: 'Geist Sans', sans-serif;
}
.input-wrapper {
width: 100%;
}
.tags-container {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 8px;
min-height: 40px;
background: ${cssManager.bdTheme('#fafafa', '#222222')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
border-radius: 8px;
transition: all 0.2s ease;
cursor: text;
}
.tags-container:focus-within {
border-color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 105, 242, 0.1)', 'rgba(0, 132, 255, 0.2)')};
}
.tags-container.disabled {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
cursor: not-allowed;
opacity: 0.6;
}
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
color: ${cssManager.bdTheme('#1976d2', '#90caf9')};
border-radius: 16px;
font-size: 14px;
line-height: 1.2;
user-select: none;
animation: tagAppear 0.2s ease;
}
@keyframes tagAppear {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.tag-remove {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
margin-left: 2px;
}
.tag-remove:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
}
.tag-remove dees-icon {
width: 12px;
height: 12px;
}
.tag-input {
flex: 1;
min-width: 120px;
border: none;
background: transparent;
outline: none;
font-size: 14px;
font-family: inherit;
color: ${cssManager.bdTheme('#333', '#fff')};
padding: 4px;
}
.tag-input::placeholder {
color: ${cssManager.bdTheme('#999', '#666')};
}
.tag-input:disabled {
cursor: not-allowed;
}
/* Suggestions dropdown */
.suggestions-container {
position: relative;
}
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: ${cssManager.bdTheme('#ffffff', '#222222')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333333')};
border-radius: 8px;
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')};
max-height: 200px;
overflow-y: auto;
z-index: 1000;
}
.suggestion {
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s ease;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.suggestion:hover,
.suggestion.highlighted {
background: ${cssManager.bdTheme('#f5f5f5', '#333333')};
}
.suggestion.highlighted {
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
}
/* Validation styles */
.validation-message {
color: #d32f2f;
font-size: 12px;
margin-top: 4px;
min-height: 16px;
}
/* Description styles */
.description {
color: ${cssManager.bdTheme('#666', '#999')};
font-size: 12px;
margin-top: 4px;
}
`,
];
public render(): TemplateResult {
const filteredSuggestions = this.suggestions.filter(
suggestion =>
!this.value.includes(suggestion) &&
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
);
return html`
<div class="input-wrapper">
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
<div class="suggestions-container">
<div
class="tags-container ${this.disabled ? 'disabled' : ''}"
@click=${this.handleContainerClick}
>
${this.value.map(tag => html`
<div class="tag">
<span>${tag}</span>
${!this.disabled ? html`
<div class="tag-remove" @click=${(e: Event) => this.removeTag(e, tag)}>
<dees-icon .icon=${'lucide:x'}></dees-icon>
</div>
` : ''}
</div>
`)}
${!this.disabled && (!this.maxTags || this.value.length < this.maxTags) ? html`
<input
type="text"
class="tag-input"
.placeholder=${this.placeholder}
.value=${this.inputValue}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
?disabled=${this.disabled}
/>
` : ''}
</div>
${this.showSuggestions && filteredSuggestions.length > 0 ? html`
<div class="suggestions-dropdown">
${filteredSuggestions.map((suggestion, index) => html`
<div
class="suggestion ${index === this.highlightedSuggestionIndex ? 'highlighted' : ''}"
@mousedown=${(e: Event) => {
e.preventDefault(); // Prevent blur
this.addTag(suggestion);
}}
@mouseenter=${() => this.highlightedSuggestionIndex = index}
>
${suggestion}
</div>
`)}
</div>
` : ''}
</div>
${this.validationText ? html`
<div class="validation-message">${this.validationText}</div>
` : ''}
${this.description ? html`
<div class="description">${this.description}</div>
` : ''}
</div>
`;
}
private handleContainerClick(e: Event) {
if (this.disabled) return;
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
if (input && e.target !== input) {
input.focus();
}
}
private handleInput(e: Event) {
const input = e.target as HTMLInputElement;
this.inputValue = input.value;
// Check for comma or semicolon to add tag
if (this.inputValue.includes(',') || this.inputValue.includes(';')) {
const tag = this.inputValue.replace(/[,;]/g, '').trim();
if (tag) {
this.addTag(tag);
}
}
}
private handleKeyDown(e: KeyboardEvent) {
const input = e.target as HTMLInputElement;
if (e.key === 'Enter') {
e.preventDefault();
if (this.highlightedSuggestionIndex >= 0 && this.showSuggestions) {
const filteredSuggestions = this.suggestions.filter(
suggestion =>
!this.value.includes(suggestion) &&
suggestion.toLowerCase().includes(this.inputValue.toLowerCase())
);
if (filteredSuggestions[this.highlightedSuggestionIndex]) {
this.addTag(filteredSuggestions[this.highlightedSuggestionIndex]);
}
} else if (this.inputValue.trim()) {
this.addTag(this.inputValue.trim());
}
} else if (e.key === 'Backspace' && !this.inputValue && this.value.length > 0) {
// Remove last tag when backspace is pressed on empty input
this.removeTag(e, this.value[this.value.length - 1]);
} else if (e.key === 'ArrowDown' && this.showSuggestions) {
e.preventDefault();
const filteredCount = this.suggestions.filter(
s => !this.value.includes(s) && s.toLowerCase().includes(this.inputValue.toLowerCase())
).length;
this.highlightedSuggestionIndex = Math.min(
this.highlightedSuggestionIndex + 1,
filteredCount - 1
);
} else if (e.key === 'ArrowUp' && this.showSuggestions) {
e.preventDefault();
this.highlightedSuggestionIndex = Math.max(this.highlightedSuggestionIndex - 1, 0);
} else if (e.key === 'Escape') {
this.showSuggestions = false;
this.highlightedSuggestionIndex = -1;
}
}
private handleFocus() {
if (this.suggestions.length > 0) {
this.showSuggestions = true;
}
}
private handleBlur() {
// Delay to allow click on suggestions
setTimeout(() => {
this.showSuggestions = false;
this.highlightedSuggestionIndex = -1;
}, 200);
}
private addTag(tag: string) {
if (!tag || this.value.includes(tag)) return;
if (this.maxTags && this.value.length >= this.maxTags) return;
this.value = [...this.value, tag];
this.inputValue = '';
this.showSuggestions = false;
this.highlightedSuggestionIndex = -1;
// Clear the input
const input = this.shadowRoot?.querySelector('.tag-input') as HTMLInputElement;
if (input) {
input.value = '';
}
this.emitChange();
}
private removeTag(e: Event, tag: string) {
e.stopPropagation();
this.value = this.value.filter(t => t !== tag);
this.emitChange();
}
private emitChange() {
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true
}));
this.changeSubject.next(this);
}
public getValue(): string[] {
return this.value;
}
public setValue(value: string[]): void {
this.value = value || [];
}
public async validate(): Promise<boolean> {
if (this.required && (!this.value || this.value.length === 0)) {
this.validationText = 'At least one tag is required';
return false;
}
this.validationText = '';
return true;
}
}

View File

@ -1,5 +1,6 @@
import { html, css, type TemplateResult } from '@design.estate/dees-element'; import { html, css, type TemplateResult } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js'; import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js';
import type { IBlock } from './wysiwyg/wysiwyg.types.js'; import type { IBlock } from './wysiwyg/wysiwyg.types.js';
@ -630,7 +631,7 @@ export const demoFunc = (): TemplateResult => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel heading="🚀 Modern WYSIWYG Editor"> <dees-panel .title=${'1. 🚀 Modern WYSIWYG Editor'}>
<p class="panel-description"> <p class="panel-description">
A powerful block-based editor with slash commands, keyboard shortcuts, and multiple output formats. A powerful block-based editor with slash commands, keyboard shortcuts, and multiple output formats.
Perfect for content creation, blog posts, documentation, and more. Perfect for content creation, blog posts, documentation, and more.
@ -708,7 +709,7 @@ export const demoFunc = (): TemplateResult => html`
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📝 Blog Post Example"> <dees-panel .title=${'2. 📝 Blog Post Example'}>
<p class="panel-description"> <p class="panel-description">
Perfect for creating rich content with multiple block types. Perfect for creating rich content with multiple block types.
The editor preserves formatting and provides a clean editing experience. The editor preserves formatting and provides a clean editing experience.
@ -722,7 +723,7 @@ export const demoFunc = (): TemplateResult => html`
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="🔀 Drag & Drop Reordering"> <dees-panel .title=${'3. 🔀 Drag & Drop Reordering'}>
<p class="panel-description"> <p class="panel-description">
Easily rearrange your content blocks by dragging them. Easily rearrange your content blocks by dragging them.
Hover over any block to reveal the drag handle on the left side. Hover over any block to reveal the drag handle on the left side.
@ -746,7 +747,7 @@ export const demoFunc = (): TemplateResult => html`
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📚 Tutorial & Documentation"> <dees-panel .title=${'4. 📚 Tutorial & Documentation'}>
<p class="panel-description"> <p class="panel-description">
Create comprehensive tutorials and documentation with code examples, lists, and structured content. Create comprehensive tutorials and documentation with code examples, lists, and structured content.
</p> </p>
@ -850,7 +851,7 @@ git merge feature-branch
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="🔄 Output Formats"> <dees-panel .title=${'5. 🔄 Output Formats'}>
<p class="panel-description"> <p class="panel-description">
Choose between HTML and Markdown output formats depending on your needs. Choose between HTML and Markdown output formats depending on your needs.
Perfect for static site generators, documentation systems, or any content management workflow. Perfect for static site generators, documentation systems, or any content management workflow.
@ -930,7 +931,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="🎨 Advanced Editing"> <dees-panel .title=${'6. 🎨 Advanced Editing'}>
<p class="panel-description"> <p class="panel-description">
Create complex documents with mixed content types. The editor handles all formatting seamlessly. Create complex documents with mixed content types. The editor handles all formatting seamlessly.
</p> </p>
@ -949,7 +950,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="⚙️ Form Integration"> <dees-panel .title=${'7. ⚙️ Form Integration'}>
<p class="panel-description"> <p class="panel-description">
Seamlessly integrates with dees-form for complete form solutions. Seamlessly integrates with dees-form for complete form solutions.
All standard form features like validation, required fields, and data binding work out of the box. All standard form features like validation, required fields, and data binding work out of the box.
@ -977,7 +978,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</dees-form> </dees-form>
</dees-panel> </dees-panel>
<dees-panel heading="🧩 Programmatic Block Creation"> <dees-panel .title=${'8. 🧩 Programmatic Block Creation'}>
<p class="panel-description"> <p class="panel-description">
Create content programmatically using the block API for dynamic document generation. Create content programmatically using the block API for dynamic document generation.
</p> </p>
@ -1003,7 +1004,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📤 Export/Import Features"> <dees-panel .title=${'9. 📤 Export/Import Features'}>
<p class="panel-description"> <p class="panel-description">
The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility. The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility.
</p> </p>

View File

@ -1,4 +1,5 @@
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js';
import { import {
cssManager, cssManager,
css, css,
@ -83,7 +84,7 @@ export class DeesMobilenavigation extends DeesElement {
min-width: 280px; min-width: 280px;
transform: translateX(200px); transform: translateX(200px);
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#333', '#fff')};
z-index: 250; z-index: ${zIndexLayers.fixed.mobileNav};
opacity: 0; opacity: 0;
padding: 16px 32px; padding: 16px 32px;
right: 0px; right: 0px;

View File

@ -1,37 +1,356 @@
import { html } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import { DeesModal } from './dees-modal.js'; import { DeesModal } from './dees-modal.js';
export const demoFunc = () => html` export const demoFunc = () => html`
<dees-button @click=${() => { <style>
DeesModal.createAndShow({ ${css`
heading: 'This is a heading', .demo-container {
content: html` display: flex;
<dees-form> flex-direction: column;
<dees-input-text gap: 24px;
.label=${'Username'} padding: 24px;
> max-width: 1200px;
</dees-input-text> margin: 0 auto;
<dees-input-text }
.label=${'Password'}
> .demo-section {
</dees-input-text> background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
</dees-form> border-radius: 8px;
`, padding: 24px;
menuOptions: [{ border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
name: 'Cancel', }
iconName: null,
action: async (deesModalArg) => { .demo-section h3 {
deesModalArg.destroy(); margin-top: 0;
return null; margin-bottom: 16px;
} color: ${cssManager.bdTheme('#333', '#fff')};
}, { }
name: 'Ok',
iconName: null, .demo-section p {
action: async (deesModalArg) => { color: ${cssManager.bdTheme('#666', '#999')};
deesModalArg.destroy(); margin-bottom: 16px;
return null; }
}
}], .button-grid {
}); display: grid;
}}>open modal</dees-button> grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Header Buttons</h3>
<p>Modals can have optional header buttons for help and closing.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'With Help Button',
showHelpButton: true,
onHelp: async () => {
const helpModal = await DeesModal.createAndShow({
heading: 'Help',
width: 'small',
showCloseButton: true,
showHelpButton: false,
content: html`
<p>This is the help content for the modal.</p>
<p>You can provide context-specific help here.</p>
`,
menuOptions: [{
name: 'Got it',
action: async (modal) => modal.destroy()
}],
});
},
content: html`
<p>This modal has a help button in the header. Click it to see help content.</p>
<p>The close button is also visible by default.</p>
`,
menuOptions: [{
name: 'OK',
action: async (modal) => modal.destroy()
}],
});
}}>With Help Button</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'No Close Button',
showCloseButton: false,
content: html`
<p>This modal has no close button in the header.</p>
<p>You must use the action buttons or click outside to close it.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>No Close Button</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Both Buttons',
showHelpButton: true,
showCloseButton: true,
onHelp: () => alert('Help clicked!'),
content: html`
<p>This modal has both help and close buttons.</p>
`,
menuOptions: [{
name: 'Done',
action: async (modal) => modal.destroy()
}],
});
}}>Both Buttons</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Clean Header',
showCloseButton: false,
showHelpButton: false,
content: html`
<p>This modal has a clean header with no buttons.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>Clean Header</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Modal Width Variations</h3>
<p>Modals can have different widths: small, medium, large, fullscreen, or custom pixel values.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Small Modal',
width: 'small',
content: html`
<p>This is a small modal with a width of 380px. Perfect for simple confirmations or brief messages.</p>
`,
menuOptions: [{
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'OK',
action: async (modal) => modal.destroy()
}],
});
}}>Small Modal</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Medium Modal (Default)',
width: 'medium',
content: html`
<dees-form>
<dees-input-text .label=${'Username'}></dees-input-text>
<dees-input-text .label=${'Email'} .inputType=${'email'}></dees-input-text>
<dees-input-text .label=${'Password'} .inputType=${'password'}></dees-input-text>
</dees-form>
`,
menuOptions: [{
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'Sign Up',
action: async (modal) => modal.destroy()
}],
});
}}>Medium Modal</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Large Modal',
width: 'large',
content: html`
<h4>Wide Content Area</h4>
<p>This large modal is 800px wide and perfect for displaying more complex content like forms with multiple columns, tables, or detailed information.</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px;">
<dees-input-text .label=${'First Name'}></dees-input-text>
<dees-input-text .label=${'Last Name'}></dees-input-text>
<dees-input-text .label=${'Company'}></dees-input-text>
<dees-input-text .label=${'Position'}></dees-input-text>
</div>
`,
menuOptions: [{
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'Save',
action: async (modal) => modal.destroy()
}],
});
}}>Large Modal</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Fullscreen Editor',
width: 'fullscreen',
showHelpButton: true,
onHelp: async () => {
alert('In a real app, this would show editor documentation');
},
content: html`
<h4>Fullscreen Experience with Header Controls</h4>
<p>This modal takes up almost the entire viewport with a 20px margin on all sides. The header buttons are particularly useful in fullscreen mode.</p>
<p>The content area can be as tall as needed and will scroll if necessary.</p>
<div style="height: 200px; background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')}; border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-top: 16px;">
<span style="color: ${cssManager.bdTheme('#999', '#666')}">Large content area</span>
</div>
`,
menuOptions: [{
name: 'Save',
action: async (modal) => modal.destroy()
}, {
name: 'Cancel',
action: async (modal) => modal.destroy()
}],
});
}}>Fullscreen Modal</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Custom Width & Constraints</h3>
<p>You can also set custom pixel widths and min/max constraints.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Custom Width (700px)',
width: 700,
content: html`
<p>This modal has a custom width of exactly 700 pixels.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>Custom 700px</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'With Max Width',
width: 'large',
maxWidth: 600,
content: html`
<p>This modal is set to 'large' but constrained by a maxWidth of 600px.</p>
`,
menuOptions: [{
name: 'Got it',
action: async (modal) => modal.destroy()
}],
});
}}>Max Width 600px</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'With Min Width',
width: 300,
minWidth: 400,
content: html`
<p>This modal width is set to 300px but has a minWidth of 400px, so it will be 400px wide.</p>
`,
menuOptions: [{
name: 'OK',
action: async (modal) => modal.destroy()
}],
});
}}>Min Width 400px</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Button Variations</h3>
<p>Modals can have different button configurations with proper spacing.</p>
<div class="button-grid">
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Multiple Actions',
content: html`
<p>This modal demonstrates multiple buttons with proper spacing between them.</p>
`,
menuOptions: [{
name: 'Delete',
action: async (modal) => modal.destroy()
}, {
name: 'Cancel',
action: async (modal) => modal.destroy()
}, {
name: 'Save Changes',
action: async (modal) => modal.destroy()
}],
});
}}>Three Buttons</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Single Action',
content: html`
<p>Sometimes you just need one button.</p>
`,
menuOptions: [{
name: 'Acknowledge',
action: async (modal) => modal.destroy()
}],
});
}}>Single Button</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'No Actions',
content: html`
<p>This modal has no bottom buttons. Use the X button or click outside to close.</p>
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')};">This is useful for informational modals that don't require user action.</p>
`,
menuOptions: [],
});
}}>No Buttons</dees-button>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Long Button Labels',
content: html`
<p>Testing button layout with longer labels.</p>
`,
menuOptions: [{
name: 'Discard All Changes',
action: async (modal) => modal.destroy()
}, {
name: 'Save and Continue Editing',
action: async (modal) => modal.destroy()
}],
});
}}>Long Labels</dees-button>
</div>
</div>
<div class="demo-section">
<h3>Responsive Behavior</h3>
<p>All modals automatically become full-width on mobile devices (< 768px viewport width) for better usability.</p>
<dees-button @click=${() => {
DeesModal.createAndShow({
heading: 'Responsive Modal',
width: 'large',
showHelpButton: true,
onHelp: () => console.log('Help requested for responsive modal'),
content: html`
<p>Resize your browser window to see how this modal adapts. On mobile viewports, it will automatically take the full width minus margins.</p>
<p>The header buttons remain accessible at all viewport sizes.</p>
`,
menuOptions: [{
name: 'Close',
action: async (modal) => modal.destroy()
}],
});
}}>Test Responsive</dees-button>
</div>
</div>
` `

View File

@ -1,5 +1,6 @@
import * as colors from './00colors.js'; import * as colors from './00colors.js';
import * as plugins from './00plugins.js'; import * as plugins from './00plugins.js';
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
import { demoFunc } from './dees-modal.demo.js'; import { demoFunc } from './dees-modal.demo.js';
import { import {
@ -18,6 +19,7 @@ import {
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from './dees-windowlayer.js'; import { DeesWindowLayer } from './dees-windowlayer.js';
import './dees-icon.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -34,12 +36,24 @@ export class DeesModal extends DeesElement {
heading: string; heading: string;
content: TemplateResult; content: TemplateResult;
menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[]; menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[];
width?: 'small' | 'medium' | 'large' | 'fullscreen' | number;
maxWidth?: number;
minWidth?: number;
showCloseButton?: boolean;
showHelpButton?: boolean;
onHelp?: () => void | Promise<void>;
}) { }) {
const body = document.body; const body = document.body;
const modal = new DeesModal(); const modal = new DeesModal();
modal.heading = optionsArg.heading; modal.heading = optionsArg.heading;
modal.content = optionsArg.content; modal.content = optionsArg.content;
modal.menuOptions = optionsArg.menuOptions; modal.menuOptions = optionsArg.menuOptions;
if (optionsArg.width) modal.width = optionsArg.width;
if (optionsArg.maxWidth) modal.maxWidth = optionsArg.maxWidth;
if (optionsArg.minWidth) modal.minWidth = optionsArg.minWidth;
if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton;
if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton;
if (optionsArg.onHelp) modal.onHelp = optionsArg.onHelp;
modal.windowLayer = await DeesWindowLayer.createAndShow({ modal.windowLayer = await DeesWindowLayer.createAndShow({
blur: true, blur: true,
}); });
@ -48,6 +62,12 @@ export class DeesModal extends DeesElement {
}); });
body.append(modal.windowLayer); body.append(modal.windowLayer);
body.append(modal); body.append(modal);
// Get z-index for modal (should be above window layer)
modal.modalZIndex = zIndexRegistry.getNextZIndex();
zIndexRegistry.register(modal, modal.modalZIndex);
return modal;
} }
// INSTANCE // INSTANCE
@ -63,6 +83,27 @@ export class DeesModal extends DeesElement {
@state({}) @state({})
public menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[] = []; public menuOptions: plugins.tsclass.website.IMenuItem<DeesModal>[] = [];
@property({ type: String })
public width: 'small' | 'medium' | 'large' | 'fullscreen' | number = 'medium';
@property({ type: Number })
public maxWidth: number;
@property({ type: Number })
public minWidth: number;
@property({ type: Boolean })
public showCloseButton: boolean = true;
@property({ type: Boolean })
public showHelpButton: boolean = false;
@property({ attribute: false })
public onHelp: () => void | Promise<void>;
@state()
private modalZIndex: number = 1000;
constructor() { constructor() {
super(); super();
} }
@ -85,13 +126,11 @@ export class DeesModal extends DeesElement {
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 2000;
} }
.modal { .modal {
will-change: transform; will-change: transform;
transform: translateY(0px) scale(0.95); transform: translateY(0px) scale(0.95);
opacity: 0; opacity: 0;
width: 480px;
min-height: 120px; min-height: 120px;
background: ${cssManager.bdTheme('#ffffff', '#111')}; background: ${cssManager.bdTheme('#ffffff', '#111')};
border-radius: 8px; border-radius: 8px;
@ -99,6 +138,33 @@ export class DeesModal extends DeesElement {
transition: all 0.2s; transition: all 0.2s;
overflow: hidden; overflow: hidden;
box-shadow: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')}; box-shadow: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')};
margin: 20px;
}
/* Width variations */
.modal.width-small {
width: 380px;
}
.modal.width-medium {
width: 560px;
}
.modal.width-large {
width: 800px;
}
.modal.width-fullscreen {
width: calc(100vw - 40px);
height: calc(100vh - 40px);
max-height: calc(100vh - 40px);
}
@media (max-width: 768px) {
.modal {
width: calc(100vw - 40px) !important;
max-width: none !important;
}
} }
.modal.show { .modal.show {
@ -112,13 +178,61 @@ export class DeesModal extends DeesElement {
} }
.modal .heading { .modal .heading {
height: 32px; height: 40px;
font-family: 'Geist Sans', sans-serif; font-family: 'Geist Sans', sans-serif;
line-height: 32px; display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
position: relative;
}
.modal .heading .header-buttons {
display: flex;
align-items: center;
gap: 4px;
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
}
.modal .heading .header-button {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
background: transparent;
color: ${cssManager.bdTheme('#666', '#999')};
}
.modal .heading .header-button:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
color: ${cssManager.bdTheme('#333', '#fff')};
}
.modal .heading .header-button:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(255, 255, 255, 0.12)')};
}
.modal .heading .header-button dees-icon {
width: 16px;
height: 16px;
display: block;
}
.modal .heading .heading-text {
flex: 1;
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 14px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; line-height: 40px;
padding: 0 40px;
} }
.modal .content { .modal .content {
@ -129,26 +243,22 @@ export class DeesModal extends DeesElement {
flex-direction: row; flex-direction: row;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
justify-content: flex-end; justify-content: flex-end;
gap: 8px;
padding: 8px;
} }
.modal .bottomButtons .bottomButton { .modal .bottomButtons .bottomButton {
margin: 8px 0px; padding: 8px 16px;
padding: 8px 12px; border-radius: 6px;
border-radius: 4px;
line-height: 16px; line-height: 16px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
font-weight: 500;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: all 0.2s; transition: all 0.2s;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
} white-space: nowrap;
.modal .bottomButtons .bottomButton:first-child {
margin-left: 8px;
}
.modal .bottomButtons .bottomButton:last-child {
margin-right: 8px;
} }
.modal .bottomButtons .bottomButton:hover { .modal .bottomButtons .bottomButton:hover {
@ -177,25 +287,46 @@ export class DeesModal extends DeesElement {
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const widthClass = typeof this.width === 'string' ? `width-${this.width}` : '';
const customWidth = typeof this.width === 'number' ? `${this.width}px` : '';
const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : '';
const minWidthStyle = this.minWidth ? `${this.minWidth}px` : '';
return html` return html`
<style> <style>
.modal .bottomButtons { ${customWidth ? `.modal { width: ${customWidth}; }` : ''}
grid-template-columns: ${cssManager.cssGridColumns(this.menuOptions.length, 0)}; ${maxWidthStyle ? `.modal { max-width: ${maxWidthStyle}; }` : ''}
} ${minWidthStyle ? `.modal { min-width: ${minWidthStyle}; }` : ''}
</style> </style>
<div class="modalContainer" @click=${this.handleOutsideClick}> <div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
<div class="modal"> <div class="modal ${widthClass}">
<div class="heading">${this.heading}</div> <div class="heading">
<div class="content">${this.content}</div> <div class="heading-text">${this.heading}</div>
<div class="bottomButtons"> <div class="header-buttons">
${this.menuOptions.map( ${this.showHelpButton ? html`
(actionArg, index) => html` <div class="header-button" @click=${this.handleHelp} title="Help">
<div class="bottomButton ${index === this.menuOptions.length - 1 ? 'primary' : ''} ${actionArg.name === 'OK' ? 'ok' : ''}" @click=${() => { <dees-icon .icon=${'lucide:helpCircle'}></dees-icon>
actionArg.action(this); </div>
}}>${actionArg.name}</div> ` : ''}
` ${this.showCloseButton ? html`
)} <div class="header-button" @click=${() => this.destroy()} title="Close">
<dees-icon .icon=${'lucide:x'}></dees-icon>
</div>
` : ''}
</div>
</div> </div>
<div class="content">${this.content}</div>
${this.menuOptions.length > 0 ? html`
<div class="bottomButtons">
${this.menuOptions.map(
(actionArg, index) => html`
<div class="bottomButton ${index === this.menuOptions.length - 1 ? 'primary' : ''} ${actionArg.name === 'OK' ? 'ok' : ''}" @click=${() => {
actionArg.action(this);
}}>${actionArg.name}</div>
`
)}
</div>
` : ''}
</div> </div>
</div> </div>
`; `;
@ -225,5 +356,14 @@ export class DeesModal extends DeesElement {
await domtools.convenience.smartdelay.delayFor(200); await domtools.convenience.smartdelay.delayFor(200);
document.body.removeChild(this); document.body.removeChild(this);
await this.windowLayer.destroy(); await this.windowLayer.destroy();
// Unregister from z-index registry
zIndexRegistry.unregister(this);
}
private async handleHelp() {
if (this.onHelp) {
await this.onHelp();
}
} }
} }

View File

@ -4,7 +4,7 @@ import './dees-form.js';
import './dees-input-text.js'; import './dees-input-text.js';
import './dees-input-checkbox.js'; import './dees-input-checkbox.js';
import './dees-input-dropdown.js'; import './dees-input-dropdown.js';
import './dees-input-radio.js'; import './dees-input-radiogroup.js';
import './dees-form-submit.js'; import './dees-form-submit.js';
import './dees-statsgrid.js'; import './dees-statsgrid.js';
import type { IStatsTile } from './dees-statsgrid.js'; import type { IStatsTile } from './dees-statsgrid.js';
@ -230,13 +230,12 @@ class DemoViewSettings extends DeesElement {
<div class="settings-section"> <div class="settings-section">
<h2>Notification Settings</h2> <h2>Notification Settings</h2>
<dees-form> <dees-form>
<div style="margin-bottom: 16px;"> <dees-input-radiogroup
<div style="font-weight: 500; margin-bottom: 8px;">Email Frequency:</div> .label=${'Email Frequency'}
<dees-input-radio label="Real-time" value="true" key="email-realtime"></dees-input-radio> .options=${['Real-time', 'Daily Digest', 'Weekly Summary', 'Never']}
<dees-input-radio label="Daily Digest" key="email-daily"></dees-input-radio> .selectedOption=${'Real-time'}
<dees-input-radio label="Weekly Summary" key="email-weekly"></dees-input-radio> .key=${'emailFrequency'}
<dees-input-radio label="Never" key="email-never"></dees-input-radio> ></dees-input-radiogroup>
</div>
<dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox> <dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox>
<dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox> <dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox>
<dees-form-submit>Update Notifications</dees-form-submit> <dees-form-submit>Update Notifications</dees-form-submit>

View File

@ -1,6 +1,7 @@
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element'; import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { zIndexLayers } from './00zindex.js';
import { demoFunc } from './dees-toast.demo.js'; import { demoFunc } from './dees-toast.demo.js';
declare global { declare global {
@ -32,7 +33,7 @@ export class DeesToast extends DeesElement {
container.className = `toast-container toast-container-${position}`; container.className = `toast-container toast-container-${position}`;
container.style.cssText = ` container.style.cssText = `
position: fixed; position: fixed;
z-index: 10000; z-index: ${zIndexLayers.overlay.toast};
pointer-events: none; pointer-events: none;
padding: 16px; padding: 16px;
display: flex; display: flex;
@ -105,6 +106,11 @@ export class DeesToast extends DeesElement {
return toast; return toast;
} }
// Alias for consistency with DeesModal
public static async createAndShow(options: IToastOptions | string) {
return this.show(options);
}
// Convenience methods // Convenience methods
public static info(message: string, duration?: number) { public static info(message: string, duration?: number) {
return this.show({ message, type: 'info', duration }); return this.show({ message, type: 'info', duration });

View File

@ -1,4 +1,5 @@
import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element'; import { customElement, DeesElement, domtools, type TemplateResult, html, property, type CSSResult, state, } from '@design.estate/dees-element';
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -32,6 +33,12 @@ export class DeesWindowLayer extends DeesElement {
public options: IOptions_DeesWindowLayer = { public options: IOptions_DeesWindowLayer = {
blur: false blur: false
}; };
@state()
private backdropZIndex: number = 1000;
@state()
private contentZIndex: number = 1001;
// INSTANCE // INSTANCE
@property({ @property({
@ -62,7 +69,7 @@ export class DeesWindowLayer extends DeesElement {
background: rgba(0, 0, 0, 0.0); background: rgba(0, 0, 0, 0.0);
backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''}; backdrop-filter: brightness(1) ${this.options.blur ? 'blur(0px)' : ''};
pointer-events: none; pointer-events: none;
z-index: 200; z-index: ${this.backdropZIndex};
} }
.slotContent { .slotContent {
position: fixed; position: fixed;
@ -71,7 +78,7 @@ export class DeesWindowLayer extends DeesElement {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 201; z-index: ${this.contentZIndex};
} }
.visible { .visible {
@ -101,9 +108,21 @@ export class DeesWindowLayer extends DeesElement {
public toggleVisibility () { public toggleVisibility () {
this.visible = !this.visible; this.visible = !this.visible;
} }
public getContentZIndex(): number {
return this.contentZIndex;
}
public async show() { public async show() {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
// Get z-indexes from registry
this.backdropZIndex = zIndexRegistry.getNextZIndex();
this.contentZIndex = zIndexRegistry.getNextZIndex();
// Register this element
zIndexRegistry.register(this, this.backdropZIndex);
await domtools.convenience.smartdelay.delayFor(0); await domtools.convenience.smartdelay.delayFor(0);
this.visible = true; this.visible = true;
} }
@ -118,6 +137,10 @@ export class DeesWindowLayer extends DeesElement {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
await this.hide(); await this.hide();
await domtools.convenience.smartdelay.delayFor(300); await domtools.convenience.smartdelay.delayFor(300);
// Unregister from z-index registry
zIndexRegistry.unregister(this);
this.remove(); this.remove();
} }
} }

View File

@ -1,3 +1,4 @@
export * from './00zindex.js';
export * from './dees-appui-activitylog.js'; export * from './dees-appui-activitylog.js';
export * from './dees-appui-appbar.js'; export * from './dees-appui-appbar.js';
export * from './dees-appui-base.js'; export * from './dees-appui-base.js';
@ -34,7 +35,9 @@ export * from './dees-input-phone.js';
export * from './dees-input-wysiwyg.js'; export * from './dees-input-wysiwyg.js';
export * from './dees-progressbar.js'; export * from './dees-progressbar.js';
export * from './dees-input-quantityselector.js'; export * from './dees-input-quantityselector.js';
export * from './dees-input-radio.js'; export * from './dees-input-radiogroup.js';
export * from './dees-input-richtext.js';
export * from './dees-input-tags.js';
export * from './dees-input-text.js'; export * from './dees-input-text.js';
export * from './dees-label.js'; export * from './dees-label.js';
export * from './dees-mobilenavigation.js'; export * from './dees-mobilenavigation.js';

View File

@ -0,0 +1,65 @@
# WYSIWYG Block Cleanup Status
## Overview
This document tracks the cleanup of `dees-wysiwyg-block.ts` after migrating all block types to the new block handler architecture.
## Completed ✅
All cleanup tasks have been successfully completed on 2025-06-26.
## Cleanup Tasks
### 1. ✅ Remove Block-Specific Styles (lines 101-219)
- [x] Remove `.block.heading-1/2/3` styles → Now in `heading.block.ts`
- [x] Remove `.block.quote` styles → Now in `quote.block.ts`
- [x] Remove `.block.list` styles → Now in `list.block.ts`
- [x] Remove `.block.paragraph` styles → Now in `paragraph.block.ts`
### 2. ✅ Remove Code Block Specific Logic
- [x] Remove code block rendering in `renderBlockContent()` (lines 508-521)
- [x] Remove all `type === 'code'` conditional branches
- [x] Simplify element selection to not special-case code blocks
### 3. ✅ Remove List Block Specific Logic
- [x] Remove `focusListItem()` method (lines 814-821)
- [x] Remove list-specific handling in `getContent()` (lines 732-734)
- [x] Remove list-specific handling in `setContent()` (lines 764-765)
- [x] Remove list content rendering in `firstUpdated()` (line 479)
### 4. ✅ Remove getPlaceholder() Method
- [x] Remove entire method (lines 538-553)
- [x] Update renderBlockContent() to not use placeholders
### 5. ✅ Clean Up Excessive Empty Lines
- [x] Remove consecutive blank lines throughout the file
### 6. ✅ Centralize nonEditableTypes
- [x] Create a single source of truth for non-editable block types
- [x] Remove duplicate arrays
### 7. ✅ Simplify Handler Delegation
- [x] Keep handler delegation pattern but ensure consistency
### 8. ✅ Remove Unused Properties (if confirmed unused)
- [x] Keep `contentInitialized` - still used for tracking
- [x] Keep `blockElement` - used for caching
- [x] Keep cursor tracking properties - used for selection
## Implementation Notes
### Block Types Now Fully Handled by Handlers:
1. **Text blocks**: paragraph, heading-1/2/3, quote, code, list
2. **Media blocks**: image, youtube, attachment
3. **Content blocks**: divider, markdown, html
### Remaining Responsibilities of dees-wysiwyg-block.ts:
1. Shadow DOM container management
2. Handler delegation for all operations
3. Generic block wrapper styles
4. Selection/cursor tracking
5. Event listener setup (until fully delegated to handlers)
## Future Improvements
- Consider moving all event handling to block handlers
- Simplify the handler delegation pattern
- Move generic block styles to a shared location
- Consider removing the need for special-casing any block types

View File

@ -23,14 +23,14 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
- All three heading levels (h1, h2, h3) using unified handler - All three heading levels (h1, h2, h3) using unified handler
- See `phase4-summary.md` for details - See `phase4-summary.md` for details
### 🔄 Phase 5: Other Text Blocks (In Progress) ### Phase 5: Other Text Blocks
- [ ] Quote block - [x] Quote block - Completed with custom styling
- [ ] Code block - [x] Code block - Completed with syntax highlighting, line numbers, and copy button
- [ ] List block - [x] List block - Completed with bullet and numbered list support
### 📋 Phase 6: Media Blocks (Planned) ### 🔄 Phase 6: Media Blocks (In Progress)
- [ ] Image block - [x] Image block - Completed with click upload, drag-drop, and base64 encoding
- [ ] YouTube block - [x] YouTube block - Completed with URL parsing and video embedding
- [ ] Attachment block - [ ] Attachment block
### 📋 Phase 7: Content Blocks (Planned) ### 📋 Phase 7: Content Blocks (Planned)
@ -46,14 +46,14 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
| heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| quote | | | | | | quote | | | | Complete with custom styling |
| code | | | | | | code | | | | Complete with highlighting, line numbers, copy |
| list | | | | | | list | | | | Complete with bullet/numbered support |
| image | | | | | | image | | | | Complete with upload, drag-drop support |
| youtube | | | | | | youtube | | | | Complete with URL parsing, video embedding |
| markdown | ❌ | ❌ | ❌ | | | attachment | ❌ | ❌ | ❌ | Phase 6 |
| html | ❌ | ❌ | ❌ | | | markdown | ❌ | ❌ | ❌ | Phase 7 |
| attachment | ❌ | ❌ | ❌ | | | html | ❌ | ❌ | ❌ | Phase 7 |
## Files Modified During Migration ## Files Modified During Migration
@ -68,11 +68,20 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
- `blocks/content/divider.block.ts` - `blocks/content/divider.block.ts`
- `blocks/text/paragraph.block.ts` - `blocks/text/paragraph.block.ts`
- `blocks/text/heading.block.ts` - `blocks/text/heading.block.ts`
- `blocks/text/quote.block.ts`
- `blocks/text/code.block.ts`
- `blocks/text/list.block.ts`
- `blocks/media/image.block.ts`
- `blocks/media/youtube.block.ts`
### Main Component Updates ### Main Component Updates
- `dees-wysiwyg-block.ts` - Updated to use registry pattern - `dees-wysiwyg-block.ts` - Updated to use registry pattern
## Next Steps ## Next Steps
1. Continue with quote block migration 1. Begin Phase 6: Media blocks migration
2. Follow established patterns from paragraph/heading handlers - Start with image block (most common media type)
3. Test thoroughly after each migration - Implement YouTube block for video embedding
- Create attachment block for file uploads
2. Follow established patterns from existing handlers
3. Test thoroughly after each migration
4. Update documentation as blocks are completed

View File

@ -1,4 +1,8 @@
import type { IBlock } from '../wysiwyg.types.js'; import type { IBlock } from '../wysiwyg.types.js';
import type { IBlockEventHandlers } from '../wysiwyg.interfaces.js';
// Re-export types from the interfaces
export type { IBlockEventHandlers } from '../wysiwyg.interfaces.js';
export interface IBlockContext { export interface IBlockContext {
shadowRoot: ShadowRoot; shadowRoot: ShadowRoot;
@ -23,15 +27,6 @@ export interface IBlockHandler {
getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null; getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null;
} }
export interface IBlockEventHandlers {
onInput: (e: InputEvent) => void;
onKeyDown: (e: KeyboardEvent) => void;
onFocus: () => void;
onBlur: () => void;
onCompositionStart: () => void;
onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void;
}
export abstract class BaseBlockHandler implements IBlockHandler { export abstract class BaseBlockHandler implements IBlockHandler {
abstract type: string; abstract type: string;

View File

@ -0,0 +1,519 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* HTMLBlockHandler - Handles raw HTML content with preview/edit toggle
*
* Features:
* - Live HTML preview (sandboxed)
* - Edit/preview mode toggle
* - Syntax highlighting in edit mode
* - HTML validation hints
* - Auto-save on mode switch
*/
export class HtmlBlockHandler extends BaseBlockHandler {
type = 'html';
render(block: IBlock, isSelected: boolean): string {
const isEditMode = block.metadata?.isEditMode ?? true;
const content = block.content || '';
return `
<div class="html-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-edit-mode="${isEditMode}">
<div class="html-header">
<div class="html-icon">&lt;/&gt;</div>
<div class="html-title">HTML</div>
<button class="html-toggle-mode" title="${isEditMode ? 'Preview' : 'Edit'}">
${isEditMode ? '👁️' : '✏️'}
</button>
</div>
<div class="html-content">
${isEditMode ? this.renderEditor(content) : this.renderPreview(content)}
</div>
</div>
`;
}
private renderEditor(content: string): string {
return `
<textarea class="html-editor"
placeholder="Enter HTML content..."
spellcheck="false">${this.escapeHtml(content)}</textarea>
`;
}
private renderPreview(content: string): string {
return `
<div class="html-preview">
${content || '<div class="preview-empty">No content to preview</div>'}
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.html-block-container') as HTMLElement;
const toggleBtn = element.querySelector('.html-toggle-mode') as HTMLButtonElement;
if (!container || !toggleBtn) {
console.error('HtmlBlockHandler: Could not find required elements');
return;
}
// Initialize metadata
if (!block.metadata) block.metadata = {};
if (block.metadata.isEditMode === undefined) block.metadata.isEditMode = true;
// Toggle mode button
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Save current content if in edit mode
if (block.metadata.isEditMode) {
const editor = container.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
block.content = editor.value;
}
}
// Toggle mode
block.metadata.isEditMode = !block.metadata.isEditMode;
// Request UI update
handlers.onRequestUpdate?.();
});
// Setup based on mode
if (block.metadata.isEditMode) {
this.setupEditor(element, block, handlers);
} else {
this.setupPreview(element, block, handlers);
}
}
private setupEditor(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (!editor) return;
// Focus handling
editor.addEventListener('focus', () => handlers.onFocus());
editor.addEventListener('blur', () => handlers.onBlur());
// Content changes
editor.addEventListener('input', () => {
block.content = editor.value;
this.validateHtml(editor.value);
});
// Keyboard shortcuts
editor.addEventListener('keydown', (e) => {
// Tab handling for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
const value = editor.value;
if (e.shiftKey) {
// Unindent
const beforeCursor = value.substring(0, start);
const lastNewline = beforeCursor.lastIndexOf('\n');
const lineStart = lastNewline + 1;
const lineContent = value.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
editor.value = value.substring(0, lineStart) + lineContent.substring(2) + value.substring(start);
editor.selectionStart = editor.selectionEnd = start - 2;
}
} else {
// Indent
editor.value = value.substring(0, start) + ' ' + value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 2;
}
block.content = editor.value;
return;
}
// Auto-close tags (Ctrl/Cmd + /)
if ((e.ctrlKey || e.metaKey) && e.key === '/') {
e.preventDefault();
this.autoCloseTag(editor);
block.content = editor.value;
return;
}
// Pass other key events to handlers
handlers.onKeyDown(e);
});
// Auto-resize
this.autoResize(editor);
editor.addEventListener('input', () => this.autoResize(editor));
}
private setupPreview(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.html-block-container') as HTMLElement;
const preview = element.querySelector('.html-preview') as HTMLElement;
if (!container || !preview) return;
// Make preview focusable
preview.setAttribute('tabindex', '0');
// Focus handling
preview.addEventListener('focus', () => handlers.onFocus());
preview.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
preview.addEventListener('keydown', (e) => {
// Switch to edit mode on Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
block.metadata.isEditMode = true;
handlers.onRequestUpdate?.();
return;
}
handlers.onKeyDown(e);
});
// Sandbox styles and scripts in preview
this.sandboxContent(preview);
}
private autoCloseTag(editor: HTMLTextAreaElement): void {
const cursorPos = editor.selectionStart;
const text = editor.value;
// Find the opening tag
let tagStart = cursorPos;
while (tagStart > 0 && text[tagStart - 1] !== '<') {
tagStart--;
}
if (tagStart > 0) {
const tagContent = text.substring(tagStart, cursorPos);
const tagMatch = tagContent.match(/^(\w+)/);
if (tagMatch) {
const tagName = tagMatch[1];
const closingTag = `</${tagName}>`;
// Insert closing tag
editor.value = text.substring(0, cursorPos) + '>' + closingTag + text.substring(cursorPos);
editor.selectionStart = editor.selectionEnd = cursorPos + 1;
}
}
}
private autoResize(editor: HTMLTextAreaElement): void {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
}
private validateHtml(html: string): boolean {
// Basic HTML validation
const openTags: string[] = [];
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g;
let match;
while ((match = tagRegex.exec(html)) !== null) {
const isClosing = match[0].startsWith('</');
const tagName = match[1].toLowerCase();
if (isClosing) {
if (openTags.length === 0 || openTags[openTags.length - 1] !== tagName) {
console.warn(`Mismatched closing tag: ${tagName}`);
return false;
}
openTags.pop();
} else if (!match[0].endsWith('/>')) {
// Not a self-closing tag
openTags.push(tagName);
}
}
if (openTags.length > 0) {
console.warn(`Unclosed tags: ${openTags.join(', ')}`);
return false;
}
return true;
}
private sandboxContent(preview: HTMLElement): void {
// Remove any script tags
const scripts = preview.querySelectorAll('script');
scripts.forEach(script => script.remove());
// Remove event handlers
const allElements = preview.querySelectorAll('*');
allElements.forEach(el => {
// Remove all on* attributes
Array.from(el.attributes).forEach(attr => {
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
});
});
// Prevent forms from submitting
const forms = preview.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
e.stopPropagation();
});
});
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
return editor.value;
}
// If in preview mode, return the stored content
const container = element.querySelector('.html-block-container');
const blockId = container?.getAttribute('data-block-id');
// In real implementation, would need access to block data
return '';
}
setContent(element: HTMLElement, content: string): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.value = content;
this.autoResize(editor);
}
}
getCursorPosition(element: HTMLElement): number | null {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
return editor ? editor.selectionStart : null;
}
setCursorToStart(element: HTMLElement): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.selectionStart = editor.selectionEnd = 0;
editor.focus();
} else {
this.focus(element);
}
}
setCursorToEnd(element: HTMLElement): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
const length = editor.value.length;
editor.selectionStart = editor.selectionEnd = length;
editor.focus();
} else {
this.focus(element);
}
}
focus(element: HTMLElement): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.focus();
} else {
const preview = element.querySelector('.html-preview') as HTMLElement;
preview?.focus();
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
if (position === 'start') {
this.setCursorToStart(element);
} else if (position === 'end') {
this.setCursorToEnd(element);
} else if (typeof position === 'number') {
editor.selectionStart = editor.selectionEnd = position;
editor.focus();
}
} else {
this.focus(element);
}
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (!editor) return null;
const cursorPos = editor.selectionStart;
return {
before: editor.value.substring(0, cursorPos),
after: editor.value.substring(cursorPos)
};
}
getStyles(): string {
return `
/* HTML Block Container */
.html-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.html-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Header */
.html-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.html-icon {
font-size: 14px;
font-weight: 600;
opacity: 0.8;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
}
.html-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.html-toggle-mode {
padding: 4px 8px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.html-toggle-mode:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Content */
.html-content {
position: relative;
min-height: 120px;
}
/* Editor */
.html-editor {
width: 100%;
min-height: 120px;
padding: 12px;
background: transparent;
border: none;
outline: none;
resize: none;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
overflow: hidden;
}
.html-editor::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Preview */
.html-preview {
padding: 12px;
min-height: 96px;
outline: none;
font-size: 14px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
}
.preview-empty {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
font-style: italic;
}
/* Sandboxed HTML preview styles */
.html-preview * {
max-width: 100%;
}
.html-preview img {
height: auto;
}
.html-preview a {
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
text-decoration: none;
}
.html-preview a:hover {
text-decoration: underline;
}
.html-preview table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
}
.html-preview th,
.html-preview td {
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
padding: 8px;
text-align: left;
}
.html-preview th {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
font-weight: 600;
}
.html-preview pre {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.html-preview code {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 2px 4px;
border-radius: 3px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
.html-preview pre code {
background: transparent;
padding: 0;
}
`;
}
}

View File

@ -0,0 +1,562 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* MarkdownBlockHandler - Handles markdown content with preview/edit toggle
*
* Features:
* - Live markdown preview
* - Edit/preview mode toggle
* - Syntax highlighting in edit mode
* - Common markdown shortcuts
* - Auto-save on mode switch
*/
export class MarkdownBlockHandler extends BaseBlockHandler {
type = 'markdown';
render(block: IBlock, isSelected: boolean): string {
const isEditMode = block.metadata?.isEditMode ?? true;
const content = block.content || '';
return `
<div class="markdown-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-edit-mode="${isEditMode}">
<div class="markdown-header">
<div class="markdown-icon">M↓</div>
<div class="markdown-title">Markdown</div>
<button class="markdown-toggle-mode" title="${isEditMode ? 'Preview' : 'Edit'}">
${isEditMode ? '👁️' : '✏️'}
</button>
</div>
<div class="markdown-content">
${isEditMode ? this.renderEditor(content) : this.renderPreview(content)}
</div>
</div>
`;
}
private renderEditor(content: string): string {
return `
<textarea class="markdown-editor"
placeholder="Enter markdown content..."
spellcheck="false">${this.escapeHtml(content)}</textarea>
`;
}
private renderPreview(content: string): string {
const html = this.parseMarkdown(content);
return `
<div class="markdown-preview">
${html || '<div class="preview-empty">No content to preview</div>'}
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.markdown-block-container') as HTMLElement;
const toggleBtn = element.querySelector('.markdown-toggle-mode') as HTMLButtonElement;
if (!container || !toggleBtn) {
console.error('MarkdownBlockHandler: Could not find required elements');
return;
}
// Initialize metadata
if (!block.metadata) block.metadata = {};
if (block.metadata.isEditMode === undefined) block.metadata.isEditMode = true;
// Toggle mode button
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Save current content if in edit mode
if (block.metadata.isEditMode) {
const editor = container.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
block.content = editor.value;
}
}
// Toggle mode
block.metadata.isEditMode = !block.metadata.isEditMode;
// Request UI update
handlers.onRequestUpdate?.();
});
// Setup based on mode
if (block.metadata.isEditMode) {
this.setupEditor(element, block, handlers);
} else {
this.setupPreview(element, block, handlers);
}
}
private setupEditor(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (!editor) return;
// Focus handling
editor.addEventListener('focus', () => handlers.onFocus());
editor.addEventListener('blur', () => handlers.onBlur());
// Content changes
editor.addEventListener('input', () => {
block.content = editor.value;
});
// Keyboard shortcuts
editor.addEventListener('keydown', (e) => {
// Tab handling for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
const value = editor.value;
if (e.shiftKey) {
// Unindent
const beforeCursor = value.substring(0, start);
const lastNewline = beforeCursor.lastIndexOf('\n');
const lineStart = lastNewline + 1;
const lineContent = value.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
editor.value = value.substring(0, lineStart) + lineContent.substring(2) + value.substring(start);
editor.selectionStart = editor.selectionEnd = start - 2;
}
} else {
// Indent
editor.value = value.substring(0, start) + ' ' + value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 2;
}
block.content = editor.value;
return;
}
// Bold shortcut (Ctrl/Cmd + B)
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
e.preventDefault();
this.wrapSelection(editor, '**', '**');
block.content = editor.value;
return;
}
// Italic shortcut (Ctrl/Cmd + I)
if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
e.preventDefault();
this.wrapSelection(editor, '_', '_');
block.content = editor.value;
return;
}
// Link shortcut (Ctrl/Cmd + K)
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
this.insertLink(editor);
block.content = editor.value;
return;
}
// Pass other key events to handlers
handlers.onKeyDown(e);
});
// Auto-resize
this.autoResize(editor);
editor.addEventListener('input', () => this.autoResize(editor));
}
private setupPreview(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.markdown-block-container') as HTMLElement;
const preview = element.querySelector('.markdown-preview') as HTMLElement;
if (!container || !preview) return;
// Make preview focusable
preview.setAttribute('tabindex', '0');
// Focus handling
preview.addEventListener('focus', () => handlers.onFocus());
preview.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
preview.addEventListener('keydown', (e) => {
// Switch to edit mode on Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
block.metadata.isEditMode = true;
handlers.onRequestUpdate?.();
return;
}
handlers.onKeyDown(e);
});
}
private wrapSelection(editor: HTMLTextAreaElement, before: string, after: string): void {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
const replacement = before + (selectedText || 'text') + after;
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
if (selectedText) {
editor.selectionStart = start;
editor.selectionEnd = start + replacement.length;
} else {
editor.selectionStart = start + before.length;
editor.selectionEnd = start + before.length + 4; // 'text'.length
}
editor.focus();
}
private insertLink(editor: HTMLTextAreaElement): void {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
const linkText = selectedText || 'link text';
const replacement = `[${linkText}](url)`;
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
// Select the URL part
editor.selectionStart = start + linkText.length + 3; // '[linktext]('.length
editor.selectionEnd = start + linkText.length + 6; // '[linktext](url'.length
editor.focus();
}
private autoResize(editor: HTMLTextAreaElement): void {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
}
private parseMarkdown(markdown: string): string {
// Basic markdown parsing - in production, use a proper markdown parser
let html = this.escapeHtml(markdown);
// Headers
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/_(.+?)_/g, '<em>$1</em>');
// Code blocks
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// Inline code
html = html.replace(/`(.+?)`/g, '<code>$1</code>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Lists
html = html.replace(/^\* (.+)$/gm, '<li>$1</li>');
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
// Wrap consecutive list items
html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => {
return '<ul>' + match + '</ul>';
});
// Paragraphs
html = html.replace(/\n\n/g, '</p><p>');
html = '<p>' + html + '</p>';
// Clean up empty paragraphs
html = html.replace(/<p><\/p>/g, '');
html = html.replace(/<p>(<h[1-3]>)/g, '$1');
html = html.replace(/(<\/h[1-3]>)<\/p>/g, '$1');
html = html.replace(/<p>(<ul>)/g, '$1');
html = html.replace(/(<\/ul>)<\/p>/g, '$1');
html = html.replace(/<p>(<pre>)/g, '$1');
html = html.replace(/(<\/pre>)<\/p>/g, '$1');
return html;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
return editor.value;
}
// If in preview mode, return the stored content
const container = element.querySelector('.markdown-block-container');
const blockId = container?.getAttribute('data-block-id');
// In real implementation, would need access to block data
return '';
}
setContent(element: HTMLElement, content: string): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.value = content;
this.autoResize(editor);
}
}
getCursorPosition(element: HTMLElement): number | null {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
return editor ? editor.selectionStart : null;
}
setCursorToStart(element: HTMLElement): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.selectionStart = editor.selectionEnd = 0;
editor.focus();
} else {
this.focus(element);
}
}
setCursorToEnd(element: HTMLElement): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
const length = editor.value.length;
editor.selectionStart = editor.selectionEnd = length;
editor.focus();
} else {
this.focus(element);
}
}
focus(element: HTMLElement): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.focus();
} else {
const preview = element.querySelector('.markdown-preview') as HTMLElement;
preview?.focus();
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
if (position === 'start') {
this.setCursorToStart(element);
} else if (position === 'end') {
this.setCursorToEnd(element);
} else if (typeof position === 'number') {
editor.selectionStart = editor.selectionEnd = position;
editor.focus();
}
} else {
this.focus(element);
}
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (!editor) return null;
const cursorPos = editor.selectionStart;
return {
before: editor.value.substring(0, cursorPos),
after: editor.value.substring(cursorPos)
};
}
getStyles(): string {
return `
/* Markdown Block Container */
.markdown-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.markdown-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Header */
.markdown-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.markdown-icon {
font-size: 14px;
font-weight: 600;
opacity: 0.8;
}
.markdown-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.markdown-toggle-mode {
padding: 4px 8px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.markdown-toggle-mode:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Content */
.markdown-content {
position: relative;
min-height: 120px;
}
/* Editor */
.markdown-editor {
width: 100%;
min-height: 120px;
padding: 12px;
background: transparent;
border: none;
outline: none;
resize: none;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
overflow: hidden;
}
.markdown-editor::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Preview */
.markdown-preview {
padding: 12px;
min-height: 96px;
outline: none;
font-size: 14px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
}
.preview-empty {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
font-style: italic;
}
/* Markdown preview styles */
.markdown-preview h1 {
font-size: 24px;
font-weight: 600;
margin: 16px 0 8px 0;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview h2 {
font-size: 20px;
font-weight: 600;
margin: 14px 0 6px 0;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview h3 {
font-size: 18px;
font-weight: 600;
margin: 12px 0 4px 0;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview p {
margin: 8px 0;
}
.markdown-preview ul,
.markdown-preview ol {
margin: 8px 0;
padding-left: 24px;
}
.markdown-preview li {
margin: 4px 0;
}
.markdown-preview code {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 2px 4px;
border-radius: 3px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
.markdown-preview pre {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.markdown-preview pre code {
background: transparent;
padding: 0;
}
.markdown-preview strong {
font-weight: 600;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview em {
font-style: italic;
}
.markdown-preview a {
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
text-decoration: none;
}
.markdown-preview a:hover {
text-decoration: underline;
}
.markdown-preview blockquote {
border-left: 3px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
padding-left: 12px;
margin: 8px 0;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
`;
}
}

View File

@ -22,22 +22,19 @@ export {
// Text block handlers // Text block handlers
export { ParagraphBlockHandler } from './text/paragraph.block.js'; export { ParagraphBlockHandler } from './text/paragraph.block.js';
export { HeadingBlockHandler } from './text/heading.block.js'; export { HeadingBlockHandler } from './text/heading.block.js';
// TODO: Export when implemented export { QuoteBlockHandler } from './text/quote.block.js';
// export { QuoteBlockHandler } from './text/quote.block.js'; export { CodeBlockHandler } from './text/code.block.js';
// export { CodeBlockHandler } from './text/code.block.js'; export { ListBlockHandler } from './text/list.block.js';
// export { ListBlockHandler } from './text/list.block.js';
// Media block handlers // Media block handlers
// TODO: Export when implemented export { ImageBlockHandler } from './media/image.block.js';
// export { ImageBlockHandler } from './media/image.block.js'; export { YouTubeBlockHandler } from './media/youtube.block.js';
// export { YoutubeBlockHandler } from './media/youtube.block.js'; export { AttachmentBlockHandler } from './media/attachment.block.js';
// export { AttachmentBlockHandler } from './media/attachment.block.js';
// Content block handlers // Content block handlers
export { DividerBlockHandler } from './content/divider.block.js'; export { DividerBlockHandler } from './content/divider.block.js';
// TODO: Export when implemented export { MarkdownBlockHandler } from './content/markdown.block.js';
// export { MarkdownBlockHandler } from './content/markdown.block.js'; export { HtmlBlockHandler } from './content/html.block.js';
// export { HtmlBlockHandler } from './content/html.block.js';
// Utilities // Utilities
// TODO: Export when implemented // TODO: Export when implemented

View File

@ -0,0 +1,477 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* AttachmentBlockHandler - Handles file attachments
*
* Features:
* - Multiple file upload support
* - Click to upload or drag and drop
* - File type icons
* - Remove individual files
* - Base64 encoding (TODO: server upload in production)
*/
export class AttachmentBlockHandler extends BaseBlockHandler {
type = 'attachment';
render(block: IBlock, isSelected: boolean): string {
const files = block.metadata?.files || [];
return `
<div class="attachment-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
tabindex="0">
<div class="attachment-header">
<div class="attachment-icon">📎</div>
<div class="attachment-title">File Attachments</div>
</div>
<div class="attachment-list">
${files.length > 0 ? this.renderFiles(files) : this.renderPlaceholder()}
</div>
<input type="file"
class="attachment-file-input"
multiple
style="display: none;" />
${files.length > 0 ? '<button class="add-more-files">Add More Files</button>' : ''}
</div>
`;
}
private renderPlaceholder(): string {
return `
<div class="attachment-placeholder">
<div class="placeholder-text">Click to add files</div>
<div class="placeholder-hint">or drag and drop</div>
</div>
`;
}
private renderFiles(files: any[]): string {
return files.map((file: any) => `
<div class="attachment-item" data-file-id="${file.id}">
<div class="file-icon">${this.getFileIcon(file.type)}</div>
<div class="file-info">
<div class="file-name">${this.escapeHtml(file.name)}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<button class="remove-file" data-file-id="${file.id}">×</button>
</div>
`).join('');
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.attachment-block-container') as HTMLElement;
const fileInput = element.querySelector('.attachment-file-input') as HTMLInputElement;
if (!container || !fileInput) {
console.error('AttachmentBlockHandler: Could not find required elements');
return;
}
// Initialize files array if needed
if (!block.metadata) block.metadata = {};
if (!block.metadata.files) block.metadata.files = [];
// Click to upload on placeholder
const placeholder = container.querySelector('.attachment-placeholder');
if (placeholder) {
placeholder.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
}
// Add more files button
const addMoreBtn = container.querySelector('.add-more-files') as HTMLButtonElement;
if (addMoreBtn) {
addMoreBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
}
// File input change
fileInput.addEventListener('change', async (e) => {
const input = e.target as HTMLInputElement;
const files = input.files;
if (files && files.length > 0) {
await this.handleFileAttachments(files, block, handlers);
input.value = ''; // Clear input for next selection
}
});
// Remove file buttons
container.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('remove-file')) {
e.preventDefault();
e.stopPropagation();
const fileId = target.getAttribute('data-file-id');
if (fileId) {
this.removeFile(fileId, block, handlers);
}
}
});
// Drag and drop
container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.add('drag-over');
});
container.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
});
container.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
await this.handleFileAttachments(files, block, handlers);
}
});
// Focus/blur
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
// Only remove all files if container is focused, not when removing individual files
if (document.activeElement === container && block.metadata?.files?.length > 0) {
e.preventDefault();
block.metadata.files = [];
handlers.onRequestUpdate?.();
return;
}
}
handlers.onKeyDown(e);
});
}
private async handleFileAttachments(
files: FileList,
block: IBlock,
handlers: IBlockEventHandlers
): Promise<void> {
if (!block.metadata) block.metadata = {};
if (!block.metadata.files) block.metadata.files = [];
for (const file of Array.from(files)) {
try {
const dataUrl = await this.fileToDataUrl(file);
const fileData = {
id: this.generateId(),
name: file.name,
size: file.size,
type: file.type,
data: dataUrl
};
block.metadata.files.push(fileData);
} catch (error) {
console.error('Failed to attach file:', file.name, error);
}
}
// Update block content with file count
block.content = `${block.metadata.files.length} file${block.metadata.files.length !== 1 ? 's' : ''} attached`;
// Request UI update
handlers.onRequestUpdate?.();
}
private removeFile(fileId: string, block: IBlock, handlers: IBlockEventHandlers): void {
if (!block.metadata?.files) return;
block.metadata.files = block.metadata.files.filter((f: any) => f.id !== fileId);
// Update content
block.content = block.metadata.files.length > 0
? `${block.metadata.files.length} file${block.metadata.files.length !== 1 ? 's' : ''} attached`
: '';
// Request UI update
handlers.onRequestUpdate?.();
}
private fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (typeof result === 'string') {
resolve(result);
} else {
reject(new Error('Failed to read file'));
}
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
private getFileIcon(mimeType: string): string {
if (mimeType.startsWith('image/')) return '🖼️';
if (mimeType.startsWith('video/')) return '🎥';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('tar')) return '🗄️';
if (mimeType.includes('sheet')) return '📊';
if (mimeType.includes('document') || mimeType.includes('msword')) return '📝';
if (mimeType.includes('presentation')) return '📋';
if (mimeType.includes('text')) return '📃';
return '📁';
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
private generateId(): string {
return `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the description of attached files
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the description of attached files
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.attachment-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// Simplified version - in real implementation would need access to block data
return {
id: blockId,
type: 'attachment',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // Attachment blocks don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.attachment-block-container') as HTMLElement;
container?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // Attachment blocks can't be split
}
getStyles(): string {
return `
/* Attachment Block Container */
.attachment-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.attachment-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
.attachment-block-container.drag-over {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* Header */
.attachment-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.attachment-icon {
font-size: 18px;
opacity: 0.8;
}
.attachment-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
/* File List */
.attachment-list {
padding: 8px;
min-height: 80px;
display: flex;
flex-direction: column;
gap: 4px;
}
/* Placeholder */
.attachment-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
cursor: pointer;
transition: all 0.15s ease;
}
.attachment-placeholder:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
}
.placeholder-text {
font-size: 14px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-bottom: 4px;
}
.placeholder-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* File Items */
.attachment-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
transition: all 0.15s ease;
}
.attachment-item:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
}
.file-icon {
font-size: 20px;
flex-shrink: 0;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 11px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-top: 2px;
}
.remove-file {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 18px;
line-height: 1;
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
}
.remove-file:hover {
background: ${cssManager.bdTheme('#fee2e2', '#991b1b')};
border-color: ${cssManager.bdTheme('#fca5a5', '#dc2626')};
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
}
/* Add More Files Button */
.add-more-files {
margin: 8px;
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
cursor: pointer;
transition: all 0.15s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.add-more-files:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Hidden file input */
.attachment-file-input {
display: none !important;
}
`;
}
}

View File

@ -0,0 +1,406 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* ImageBlockHandler - Handles image upload, display, and interactions
*
* Features:
* - Click to upload
* - Drag and drop support
* - Base64 encoding (TODO: server upload in production)
* - Loading states
* - Alt text from filename
*/
export class ImageBlockHandler extends BaseBlockHandler {
type = 'image';
render(block: IBlock, isSelected: boolean): string {
const imageUrl = block.metadata?.url;
const altText = block.content || 'Image';
const isLoading = block.metadata?.loading;
return `
<div class="image-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-has-image="${!!imageUrl}"
tabindex="0">
${isLoading ? this.renderLoading() :
imageUrl ? this.renderImage(imageUrl, altText) :
this.renderPlaceholder()}
<input type="file"
class="image-file-input"
accept="image/*"
style="display: none;" />
</div>
`;
}
private renderPlaceholder(): string {
return `
<div class="image-upload-placeholder" style="cursor: pointer;">
<div class="upload-icon" style="pointer-events: none;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</div>
<div class="upload-text" style="pointer-events: none;">Click to upload an image</div>
<div class="upload-hint" style="pointer-events: none;">or drag and drop</div>
</div>
`;
}
private renderImage(url: string, altText: string): string {
return `
<div class="image-container">
<img src="${url}" alt="${this.escapeHtml(altText)}" />
</div>
`;
}
private renderLoading(): string {
return `
<div class="image-loading">
<div class="loading-spinner"></div>
<div class="loading-text">Uploading image...</div>
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.image-block-container') as HTMLElement;
const fileInput = element.querySelector('.image-file-input') as HTMLInputElement;
if (!container) {
console.error('ImageBlockHandler: Could not find container');
return;
}
if (!fileInput) {
console.error('ImageBlockHandler: Could not find file input');
return;
}
// Click to upload (only on placeholder)
const placeholder = container.querySelector('.image-upload-placeholder');
if (placeholder) {
placeholder.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('ImageBlockHandler: Placeholder clicked, opening file selector');
fileInput.click();
});
}
// Container click for focus
container.addEventListener('click', () => {
handlers.onFocus();
});
// File input change
fileInput.addEventListener('change', async (e) => {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
console.log('ImageBlockHandler: File selected:', file.name);
await this.handleFileUpload(file, block, handlers);
}
});
// Drag and drop
container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
if (!block.metadata?.url) {
container.classList.add('drag-over');
}
});
container.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
});
container.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
const file = e.dataTransfer?.files[0];
if (file && file.type.startsWith('image/') && !block.metadata?.url) {
await this.handleFileUpload(file, block, handlers);
}
});
// Focus/blur
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (block.metadata?.url) {
// Clear the image
block.metadata.url = undefined;
block.metadata.loading = false;
block.content = '';
handlers.onInput(new InputEvent('input'));
return;
}
}
handlers.onKeyDown(e);
});
}
private async handleFileUpload(
file: File,
block: IBlock,
handlers: IBlockEventHandlers
): Promise<void> {
console.log('ImageBlockHandler: Starting file upload', {
fileName: file.name,
fileSize: file.size,
blockId: block.id
});
// Validate file
if (!file.type.startsWith('image/')) {
console.error('Invalid file type:', file.type);
return;
}
// Check file size (10MB limit)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
console.error('File too large. Maximum size is 10MB');
return;
}
// Set loading state
if (!block.metadata) block.metadata = {};
block.metadata.loading = true;
block.metadata.fileName = file.name;
block.metadata.fileSize = file.size;
block.metadata.mimeType = file.type;
console.log('ImageBlockHandler: Set loading state, requesting update');
// Request immediate UI update for loading state
handlers.onRequestUpdate?.();
try {
// Convert to base64
const dataUrl = await this.fileToDataUrl(file);
// Update block
block.metadata.url = dataUrl;
block.metadata.loading = false;
// Set default alt text from filename
const nameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
block.content = nameWithoutExt;
console.log('ImageBlockHandler: Upload complete, requesting update', {
hasUrl: !!block.metadata.url,
urlLength: dataUrl.length,
altText: block.content
});
// Request immediate UI update to show uploaded image
handlers.onRequestUpdate?.();
} catch (error) {
console.error('Failed to upload image:', error);
block.metadata.loading = false;
// Request UI update to clear loading state
handlers.onRequestUpdate?.();
}
}
private fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (typeof result === 'string') {
resolve(result);
} else {
reject(new Error('Failed to read file'));
}
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the alt text
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the alt text
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.image-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// This is a simplified version - in real implementation,
// we'd need access to the block data
return {
id: blockId,
type: 'image',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // Images don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.image-block-container') as HTMLElement;
container?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // Images can't be split
}
getStyles(): string {
return `
/* Image Block Container */
.image-block-container {
position: relative;
margin: 12px 0;
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
cursor: pointer;
}
.image-block-container.selected {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* Upload Placeholder */
.image-upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
border: 2px dashed ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
transition: all 0.15s ease;
}
.image-block-container:hover .image-upload-placeholder {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
background: ${cssManager.bdTheme('#f9fafb', '#111827')};
}
.image-block-container.drag-over .image-upload-placeholder {
border-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
background: ${cssManager.bdTheme('#eff6ff', '#1e1b4b')};
}
.upload-icon {
margin-bottom: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
}
.upload-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
margin-bottom: 4px;
}
.upload-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Image Container */
.image-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
background: ${cssManager.bdTheme('#f9fafb', '#111827')};
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
border-radius: 4px;
}
/* Loading State */
.image-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-top-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 14px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
/* File input hidden */
.image-file-input {
display: none !important;
}
`;
}
}

View File

@ -0,0 +1,337 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* YouTubeBlockHandler - Handles YouTube video embedding
*
* Features:
* - YouTube URL parsing and validation
* - Video ID extraction from various YouTube URL formats
* - Embedded iframe player
* - Clean minimalist design
*/
export class YouTubeBlockHandler extends BaseBlockHandler {
type = 'youtube';
render(block: IBlock, isSelected: boolean): string {
const videoId = block.metadata?.videoId;
const url = block.metadata?.url || '';
return `
<div class="youtube-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-has-video="${!!videoId}">
${videoId ? this.renderVideo(videoId) : this.renderPlaceholder(url)}
</div>
`;
}
private renderPlaceholder(url: string): string {
return `
<div class="youtube-placeholder">
<div class="placeholder-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
</svg>
</div>
<div class="placeholder-text">Enter YouTube URL</div>
<input type="url"
class="youtube-url-input"
placeholder="https://youtube.com/watch?v=..."
value="${this.escapeHtml(url)}" />
<button class="youtube-embed-btn">Embed Video</button>
</div>
`;
}
private renderVideo(videoId: string): string {
return `
<div class="youtube-container">
<iframe
src="https://www.youtube.com/embed/${videoId}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.youtube-block-container') as HTMLElement;
if (!container) return;
// If video is already embedded, just handle focus/blur
if (block.metadata?.videoId) {
container.setAttribute('tabindex', '0');
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Handle deletion
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
handlers.onKeyDown(e);
} else {
handlers.onKeyDown(e);
}
});
return;
}
// Setup placeholder interactions
const urlInput = element.querySelector('.youtube-url-input') as HTMLInputElement;
const embedBtn = element.querySelector('.youtube-embed-btn') as HTMLButtonElement;
if (!urlInput || !embedBtn) return;
// Focus management
urlInput.addEventListener('focus', () => handlers.onFocus());
urlInput.addEventListener('blur', () => handlers.onBlur());
// Handle embed button click
embedBtn.addEventListener('click', () => {
this.embedVideo(urlInput.value, block, handlers);
});
// Handle Enter key in input
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.embedVideo(urlInput.value, block, handlers);
} else if (e.key === 'Escape') {
e.preventDefault();
urlInput.blur();
}
});
// Handle paste event
urlInput.addEventListener('paste', (e) => {
// Allow paste to complete first
setTimeout(() => {
const pastedUrl = urlInput.value;
if (this.extractYouTubeVideoId(pastedUrl)) {
// Auto-embed if valid YouTube URL was pasted
this.embedVideo(pastedUrl, block, handlers);
}
}, 0);
});
// Update URL in metadata as user types
urlInput.addEventListener('input', () => {
if (!block.metadata) block.metadata = {};
block.metadata.url = urlInput.value;
});
}
private embedVideo(url: string, block: IBlock, handlers: IBlockEventHandlers): void {
const videoId = this.extractYouTubeVideoId(url);
if (!videoId) {
// Could show an error message here
console.error('Invalid YouTube URL');
return;
}
// Update block metadata
if (!block.metadata) block.metadata = {};
block.metadata.videoId = videoId;
block.metadata.url = url;
// Set content as video title (could be fetched from API in the future)
block.content = `YouTube Video: ${videoId}`;
// Request immediate UI update to show embedded video
handlers.onRequestUpdate?.();
}
private extractYouTubeVideoId(url: string): string | null {
// Handle various YouTube URL formats
const patterns = [
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/,
/youtube\.com\/embed\/([^"&?\/ ]{11})/,
/youtube\.com\/watch\?v=([^"&?\/ ]{11})/,
/youtu\.be\/([^"&?\/ ]{11})/
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) {
return match[1];
}
}
return null;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the video description/title
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the video description/title
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.youtube-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// Simplified version - in real implementation would need access to block data
return {
id: blockId,
type: 'youtube',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // YouTube blocks don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.youtube-block-container') as HTMLElement;
const urlInput = element.querySelector('.youtube-url-input') as HTMLInputElement;
if (urlInput) {
urlInput.focus();
} else if (container) {
container.focus();
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // YouTube blocks can't be split
}
getStyles(): string {
return `
/* YouTube Block Container */
.youtube-block-container {
position: relative;
margin: 12px 0;
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
}
.youtube-block-container.selected {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* YouTube Placeholder */
.youtube-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 24px;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
gap: 12px;
}
.placeholder-icon {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
opacity: 0.8;
}
.placeholder-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.youtube-url-input {
width: 100%;
max-width: 400px;
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: all 0.15s ease;
outline: none;
}
.youtube-url-input:focus {
border-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
background: ${cssManager.bdTheme('#ffffff', '#1f2937')};
}
.youtube-url-input::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
}
.youtube-embed-btn {
padding: 6px 16px;
background: ${cssManager.bdTheme('#111827', '#f9fafb')};
color: ${cssManager.bdTheme('#f9fafb', '#111827')};
border: 1px solid transparent;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.youtube-embed-btn:hover {
background: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.youtube-embed-btn:active {
transform: scale(0.98);
}
/* YouTube Container */
.youtube-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
background: ${cssManager.bdTheme('#000000', '#000000')};
}
.youtube-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
border-radius: 6px;
}
`;
}
}

View File

@ -1,145 +1,217 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js'; import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js'; import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element'; import { cssManager } from '@design.estate/dees-element';
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
import { WysiwygSelection } from '../../wysiwyg.selection.js'; import { WysiwygSelection } from '../../wysiwyg.selection.js';
import hlight from 'highlight.js';
/**
* CodeBlockHandler with improved architecture
*
* Key features:
* 1. Simple DOM structure
* 2. Line number handling
* 3. Syntax highlighting only when not focused (grey text while editing)
* 4. Clean event handling
* 5. Copy button functionality
*/
export class CodeBlockHandler extends BaseBlockHandler { export class CodeBlockHandler extends BaseBlockHandler {
type = 'code'; type = 'code';
// Track cursor position private highlightTimer: any = null;
private lastKnownCursorPosition: number = 0;
render(block: IBlock, isSelected: boolean): string { render(block: IBlock, isSelected: boolean): string {
const language = block.metadata?.language || 'plain text'; const language = block.metadata?.language || 'javascript';
const selectedClass = isSelected ? ' selected' : ''; const content = block.content || '';
const lineCount = content.split('\n').length;
console.log('CodeBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, language }); // Generate line numbers
let lineNumbersHtml = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHtml += `<div class="line-number">${i}</div>`;
}
return ` return `
<div class="code-block-container"> <div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
<div class="code-language">${language}</div> <div class="code-header">
<div <span class="language-label">${language}</span>
class="block code${selectedClass}" <button class="copy-button" title="Copy code">
contenteditable="true" <svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
data-block-id="${block.id}" <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
data-block-type="${block.type}" <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path>
spellcheck="false" </svg>
></div> <span class="copy-text">Copy</span>
</button>
</div>
<div class="code-body">
<div class="line-numbers">${lineNumbersHtml}</div>
<div class="code-content">
<pre class="code-pre"><code class="code-editor"
contenteditable="true"
data-block-id="${block.id}"
data-block-type="${block.type}"
spellcheck="false">${this.escapeHtml(content)}</code></pre>
</div>
</div>
</div> </div>
`; `;
} }
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement; const editor = element.querySelector('.code-editor') as HTMLElement;
if (!codeBlock) { const container = element.querySelector('.code-block-container') as HTMLElement;
console.error('CodeBlockHandler.setup: No code block element found'); const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
return;
if (!editor || !container) return;
// Setup copy button
if (copyButton) {
copyButton.addEventListener('click', async () => {
const content = editor.textContent || '';
try {
await navigator.clipboard.writeText(content);
// Show feedback
const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
const originalText = copyText.textContent;
copyText.textContent = 'Copied!';
copyButton.classList.add('copied');
// Reset after 2 seconds
setTimeout(() => {
copyText.textContent = originalText;
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
// @ts-ignore - execCommand is deprecated but needed for fallback
document.execCommand('copy');
// Show feedback
const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
const originalText = copyText.textContent;
copyText.textContent = 'Copied!';
copyButton.classList.add('copied');
setTimeout(() => {
copyText.textContent = originalText;
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Fallback copy failed:', err);
}
document.body.removeChild(textArea);
}
});
} }
console.log('CodeBlockHandler.setup: Setting up code block', { blockId: block.id }); // Track if we're currently editing
let isEditing = false;
// Set initial content if needed - use textContent for code blocks // Focus handler
if (block.content && !codeBlock.textContent) { editor.addEventListener('focus', () => {
codeBlock.textContent = block.content; isEditing = true;
} container.classList.add('editing');
// Remove all syntax highlighting when focused
const content = editor.textContent || '';
editor.textContent = content; // This removes all HTML formatting
// Restore cursor position after removing highlighting
requestAnimationFrame(() => {
const range = document.createRange();
const selection = window.getSelection();
if (editor.firstChild) {
range.setStart(editor.firstChild, 0);
range.collapse(true);
selection?.removeAllRanges();
selection?.addRange(range);
}
});
handlers.onFocus();
});
// Blur handler
editor.addEventListener('blur', () => {
isEditing = false;
container.classList.remove('editing');
// Apply final highlighting on blur
this.applyHighlighting(element, block);
handlers.onBlur();
});
// Input handler // Input handler
codeBlock.addEventListener('input', (e) => { editor.addEventListener('input', (e) => {
console.log('CodeBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Update line numbers
const pos = this.getCursorPosition(element); this.updateLineNumbers(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos; // Clear any pending highlight timer (no highlighting while editing)
} clearTimeout(this.highlightTimer);
}); });
// Keydown handler // Keydown handler
codeBlock.addEventListener('keydown', (e) => { editor.addEventListener('keydown', (e) => {
// Track cursor position before keydown // Handle Tab key for code blocks
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
// Special handling for Tab key in code blocks
if (e.key === 'Tab') { if (e.key === 'Tab') {
e.preventDefault(); e.preventDefault();
// Insert two spaces for tab
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(' '); const textNode = document.createTextNode(' ');
range.insertNode(textNode); range.insertNode(textNode);
range.setStartAfter(textNode); range.setStartAfter(textNode);
range.setEndAfter(textNode); range.setEndAfter(textNode);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
// Trigger input event
handlers.onInput(new InputEvent('input')); handlers.onInput(new InputEvent('input'));
this.updateLineNumbers(element);
} }
return; return;
} }
// Check cursor position for navigation keys
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
const cursorPos = this.getCursorPosition(element);
const textLength = editor.textContent?.length || 0;
// For ArrowLeft at position 0 or ArrowRight at end, let parent handle navigation
if ((e.key === 'ArrowLeft' && cursorPos === 0) ||
(e.key === 'ArrowRight' && cursorPos === textLength)) {
// Pass to parent handler for inter-block navigation
handlers.onKeyDown(e);
return;
}
// For ArrowUp/Down, check if we're at first/last line
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
const lines = (editor.textContent || '').split('\n');
const currentLine = this.getCurrentLineIndex(editor);
if ((e.key === 'ArrowUp' && currentLine === 0) ||
(e.key === 'ArrowDown' && currentLine === lines.length - 1)) {
// Let parent handle navigation to prev/next block
handlers.onKeyDown(e);
return;
}
}
}
// Pass other keys to parent handler
handlers.onKeyDown(e); handlers.onKeyDown(e);
}); });
// Focus handler // Paste handler - plain text only
codeBlock.addEventListener('focus', () => { editor.addEventListener('paste', (e) => {
console.log('CodeBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus();
});
// Blur handler
codeBlock.addEventListener('blur', () => {
console.log('CodeBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur();
});
// Composition handlers for IME support
codeBlock.addEventListener('compositionstart', () => {
console.log('CodeBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart();
});
codeBlock.addEventListener('compositionend', () => {
console.log('CodeBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd();
});
// Mouse up handler
codeBlock.addEventListener('mouseup', (e) => {
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
handlers.onMouseUp?.(e);
});
// Click handler with delayed cursor tracking
codeBlock.addEventListener('click', (e: MouseEvent) => {
setTimeout(() => {
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
}, 0);
});
// Keyup handler for cursor tracking
codeBlock.addEventListener('keyup', (e) => {
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
});
// Paste handler - handle as plain text
codeBlock.addEventListener('paste', (e) => {
e.preventDefault(); e.preventDefault();
const text = e.clipboardData?.getData('text/plain'); const text = e.clipboardData?.getData('text/plain');
if (text) { if (text) {
@ -153,259 +225,445 @@ export class CodeBlockHandler extends BaseBlockHandler {
range.setEndAfter(textNode); range.setEndAfter(textNode);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
// Trigger input event
handlers.onInput(new InputEvent('input')); handlers.onInput(new InputEvent('input'));
this.updateLineNumbers(element);
} }
} }
}); });
// Composition handlers
editor.addEventListener('compositionstart', () => handlers.onCompositionStart());
editor.addEventListener('compositionend', () => handlers.onCompositionEnd());
// Initial syntax highlighting if content exists and not focused
if (block.content && document.activeElement !== editor) {
requestAnimationFrame(() => {
this.applyHighlighting(element, block);
});
}
}
private updateLineNumbers(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
const lineNumbersContainer = element.querySelector('.line-numbers') as HTMLElement;
if (!editor || !lineNumbersContainer) return;
const content = editor.textContent || '';
const lines = content.split('\n');
const lineCount = lines.length || 1;
let lineNumbersHtml = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHtml += `<div class="line-number">${i}</div>`;
}
lineNumbersContainer.innerHTML = lineNumbersHtml;
}
private getCurrentLineIndex(editor: HTMLElement): number {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return 0;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editor);
preCaretRange.setEnd(range.startContainer, range.startOffset);
const textBeforeCursor = preCaretRange.toString();
const linesBeforeCursor = textBeforeCursor.split('\n');
return linesBeforeCursor.length - 1; // 0-indexed
}
private applyHighlighting(element: HTMLElement, block: IBlock): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return;
// Store cursor position
const cursorPos = this.getCursorPosition(element);
// Get plain text content
const content = editor.textContent || '';
const language = block.metadata?.language || 'javascript';
// Apply highlighting
try {
const result = hlight.highlight(content, {
language: language,
ignoreIllegals: true
});
// Only update if we have valid highlighted content
if (result.value) {
editor.innerHTML = result.value;
// Restore cursor position if editor is focused
if (document.activeElement === editor && cursorPos !== null) {
requestAnimationFrame(() => {
WysiwygSelection.setCursorPosition(editor, cursorPos);
});
}
}
} catch (error) {
// If highlighting fails, keep plain text
console.warn('Syntax highlighting failed:', error);
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
const editor = element.querySelector('.code-editor') as HTMLElement;
return editor?.textContent || '';
}
setContent(element: HTMLElement, content: string): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return;
editor.textContent = content;
this.updateLineNumbers(element);
// Apply highlighting if not focused
if (document.activeElement !== editor) {
const block: IBlock = {
id: editor.dataset.blockId || '',
type: 'code',
content: content,
metadata: {
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'javascript'
}
};
this.applyHighlighting(element, block);
}
}
getCursorPosition(element: HTMLElement): number | null {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return null;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
if (!editor.contains(range.startContainer)) return null;
const preCaretRange = document.createRange();
preCaretRange.selectNodeContents(editor);
preCaretRange.setEnd(range.startContainer, range.startOffset);
return preCaretRange.toString().length;
}
setCursorToStart(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (editor) {
WysiwygSelection.setCursorPosition(editor, 0);
}
}
setCursorToEnd(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (editor) {
const length = editor.textContent?.length || 0;
WysiwygSelection.setCursorPosition(editor, length);
}
}
focus(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
editor?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return;
editor.focus();
requestAnimationFrame(() => {
if (position === 'start') {
this.setCursorToStart(element);
} else if (position === 'end') {
this.setCursorToEnd(element);
} else if (typeof position === 'number') {
WysiwygSelection.setCursorPosition(editor, position);
}
});
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
const position = this.getCursorPosition(element);
if (position === null) return null;
const content = this.getContent(element);
return {
before: content.substring(0, position),
after: content.substring(position)
};
} }
getStyles(): string { getStyles(): string {
return ` return `
/* Code block specific styles */ /* Code Block Container - Minimalist shadcn style */
.code-block-container { .code-block-container {
position: relative; position: relative;
margin: 20px 0; margin: 12px 0;
} background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
.block.code {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px;
background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
padding: 16px 20px;
padding-top: 32px;
border-radius: 6px; border-radius: 6px;
white-space: pre-wrap; overflow: hidden;
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')}; transition: all 0.15s ease;
line-height: 1.5;
overflow-x: auto;
margin: 0;
} }
.code-language { .code-block-container.selected {
position: absolute; border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
top: 0; }
right: 0;
background: ${cssManager.bdTheme('#e1e4e8', '#333333')}; .code-block-container.editing {
color: ${cssManager.bdTheme('#586069', '#8b949e')}; border-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
padding: 4px 12px; background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
/* Header - Simplified */
.code-header {
background: transparent;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.language-label {
font-size: 12px; font-size: 12px;
border-radius: 0 6px 0 6px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-weight: 500;
text-transform: lowercase; text-transform: uppercase;
z-index: 1; letter-spacing: 0.05em;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Copy Button - Minimal */
.copy-button {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.copy-button:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.copy-button:active {
transform: scale(0.98);
}
.copy-button.copied {
color: ${cssManager.bdTheme('#059669', '#10b981')};
}
.copy-icon {
flex-shrink: 0;
opacity: 0.7;
}
.copy-button:hover .copy-icon {
opacity: 1;
}
.copy-text {
min-width: 40px;
text-align: center;
}
/* Code Body */
.code-body {
display: flex;
position: relative;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
/* Line Numbers - Subtle */
.line-numbers {
flex-shrink: 0;
padding: 12px 0;
background: transparent;
text-align: right;
user-select: none;
min-width: 40px;
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
}
.line-number {
padding: 0 12px 0 8px;
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 13px;
line-height: 20px;
height: 20px;
}
/* Code Content */
.code-content {
flex: 1;
overflow-x: auto;
position: relative;
}
.code-pre {
margin: 0;
padding: 0;
background: transparent;
}
.code-editor {
display: block;
padding: 12px 16px;
margin: 0;
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 13px;
line-height: 20px;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
background: transparent;
border: none;
outline: none;
white-space: pre-wrap;
word-wrap: break-word;
min-height: 60px;
overflow: visible;
}
/* Placeholder */
.code-editor:empty::before {
content: "// Type or paste code here...";
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
pointer-events: none;
}
/* When editing (focused), show grey text without highlighting */
.code-block-container.editing .code-editor {
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')} !important;
}
.code-block-container.editing .code-editor * {
color: inherit !important;
}
/* Syntax Highlighting - Muted colors */
.code-editor .hljs-keyword {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
font-weight: 500;
}
.code-editor .hljs-string {
color: ${cssManager.bdTheme('#059669', '#10b981')};
}
.code-editor .hljs-number {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
.code-editor .hljs-function {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.code-editor .hljs-comment {
color: ${cssManager.bdTheme('#6b7280', '#6b7280')};
font-style: italic;
}
.code-editor .hljs-variable,
.code-editor .hljs-attr {
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
.code-editor .hljs-class,
.code-editor .hljs-title {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
font-weight: 500;
}
.code-editor .hljs-params {
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.code-editor .hljs-built_in {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
.code-editor .hljs-literal {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
.code-editor .hljs-meta {
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
.code-editor .hljs-punctuation {
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
}
.code-editor .hljs-tag {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.code-editor .hljs-attribute {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.code-editor .hljs-selector-tag {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.code-editor .hljs-selector-class {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.code-editor .hljs-selector-id {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
/* Selection */
.code-editor::selection,
.code-editor *::selection {
background: ${cssManager.bdTheme('rgba(99, 102, 241, 0.2)', 'rgba(99, 102, 241, 0.3)')};
}
/* Scrollbar styling - Minimal */
.code-content::-webkit-scrollbar {
height: 6px;
}
.code-content::-webkit-scrollbar-track {
background: transparent;
}
.code-content::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
border-radius: 3px;
}
.code-content::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
} }
`; `;
} }
getPlaceholder(): string {
return '';
}
// Helper methods for code functionality
getCursorPosition(element: HTMLElement, context?: any): number | null {
// Get the actual code element
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) {
console.log('CodeBlockHandler.getCursorPosition: No code element found');
return null;
}
// Get shadow roots from context
const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
const blockShadowRoot = context?.shadowRoot;
// Get selection info with both shadow roots for proper traversal
const shadowRoots: ShadowRoot[] = [];
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
if (!selectionInfo) {
return null;
}
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
return null;
}
// Create a range from start of element to cursor position
const preCaretRange = document.createRange();
preCaretRange.selectNodeContents(codeBlock);
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
// Get the text content length up to cursor
const position = preCaretRange.toString().length;
return position;
}
getContent(element: HTMLElement, context?: any): string {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return '';
// For code blocks, get textContent to avoid HTML formatting
const content = codeBlock.textContent || '';
console.log('CodeBlockHandler.getContent:', content);
return content;
}
setContent(element: HTMLElement, content: string, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return;
// Store if we have focus
const hadFocus = document.activeElement === codeBlock ||
element.shadowRoot?.activeElement === codeBlock;
// Use textContent for code blocks
codeBlock.textContent = content;
// Restore focus if we had it
if (hadFocus) {
codeBlock.focus();
}
}
setCursorToStart(element: HTMLElement, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (codeBlock) {
WysiwygBlocks.setCursorToStart(codeBlock);
}
}
setCursorToEnd(element: HTMLElement, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (codeBlock) {
WysiwygBlocks.setCursorToEnd(codeBlock);
}
}
focus(element: HTMLElement, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return;
// Ensure the element is focusable
if (!codeBlock.hasAttribute('contenteditable')) {
codeBlock.setAttribute('contenteditable', 'true');
}
codeBlock.focus();
// If focus failed, try again after a microtask
if (document.activeElement !== codeBlock && element.shadowRoot?.activeElement !== codeBlock) {
Promise.resolve().then(() => {
codeBlock.focus();
});
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return;
// Ensure element is focusable first
if (!codeBlock.hasAttribute('contenteditable')) {
codeBlock.setAttribute('contenteditable', 'true');
}
// Focus the element
codeBlock.focus();
// Set cursor position after focus is established
const setCursor = () => {
if (position === 'start') {
this.setCursorToStart(element, context);
} else if (position === 'end') {
this.setCursorToEnd(element, context);
} else if (typeof position === 'number') {
// Use the selection utility to set cursor position
WysiwygSelection.setCursorPosition(codeBlock, position);
}
};
// Ensure cursor is set after focus
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
setCursor();
} else {
// Wait for focus to be established
Promise.resolve().then(() => {
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
setCursor();
}
});
}
}
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) {
return null;
}
// Get shadow roots from context
const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
const blockShadowRoot = context?.shadowRoot;
// Get selection info with both shadow roots for proper traversal
const shadowRoots: ShadowRoot[] = [];
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
if (!selectionInfo) {
// Try using last known cursor position
if (this.lastKnownCursorPosition !== null) {
const fullText = codeBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
return {
before: fullText.substring(0, pos),
after: fullText.substring(pos)
};
}
return null;
}
// Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
// Try using last known cursor position
if (this.lastKnownCursorPosition !== null) {
const fullText = codeBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
return {
before: fullText.substring(0, pos),
after: fullText.substring(pos)
};
}
return null;
}
// Get cursor position
const cursorPos = this.getCursorPosition(element, context);
if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content
return {
before: '',
after: codeBlock.textContent || ''
};
}
// For code blocks, split based on text content only
const fullText = codeBlock.textContent || '';
return {
before: fullText.substring(0, cursorPos),
after: fullText.substring(cursorPos)
};
}
} }

View File

@ -23,7 +23,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const selectedClass = isSelected ? ' selected' : ''; const selectedClass = isSelected ? ' selected' : '';
const placeholder = this.getPlaceholder(); const placeholder = this.getPlaceholder();
console.log('HeadingBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, level: this.level });
return ` return `
<div <div
@ -43,7 +42,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('HeadingBlockHandler.setup: Setting up heading block', { blockId: block.id, level: this.level });
// Set initial content if needed // Set initial content if needed
if (block.content && !headingBlock.innerHTML) { if (block.content && !headingBlock.innerHTML) {
@ -52,14 +50,12 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Input handler with cursor tracking // Input handler with cursor tracking
headingBlock.addEventListener('input', (e) => { headingBlock.addEventListener('input', (e) => {
console.log('HeadingBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Updated cursor position after input', { pos });
} }
}); });
@ -69,7 +65,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position before keydown', { pos, key: e.key });
} }
handlers.onKeyDown(e); handlers.onKeyDown(e);
@ -77,24 +72,20 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
headingBlock.addEventListener('focus', () => { headingBlock.addEventListener('focus', () => {
console.log('HeadingBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
headingBlock.addEventListener('blur', () => { headingBlock.addEventListener('blur', () => {
console.log('HeadingBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
headingBlock.addEventListener('compositionstart', () => { headingBlock.addEventListener('compositionstart', () => {
console.log('HeadingBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
headingBlock.addEventListener('compositionend', () => { headingBlock.addEventListener('compositionend', () => {
console.log('HeadingBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -103,7 +94,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position after mouseup', { pos });
} }
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
@ -117,7 +107,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position after click', { pos });
} }
}, 0); }, 0);
}); });
@ -127,7 +116,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position after keyup', { pos, key: e.key });
} }
}); });
@ -178,11 +166,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('HeadingBlockHandler: Text selected', {
text: selectedText,
blockId: block.id
});
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
@ -302,7 +285,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Get the actual heading element // Get the actual heading element
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
if (!headingBlock) { if (!headingBlock) {
console.log('HeadingBlockHandler.getCursorPosition: No heading element found');
return null; return null;
} }
@ -318,25 +300,12 @@ export class HeadingBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('HeadingBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('HeadingBlockHandler.getCursorPosition: No selection found');
return null; return null;
} }
console.log('HeadingBlockHandler.getCursorPosition: Range info:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed,
startContainerText: selectionInfo.startContainer.textContent
});
if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) {
console.log('HeadingBlockHandler.getCursorPosition: Range not in element');
return null; return null;
} }
@ -347,12 +316,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Get the text content length up to cursor // Get the text content length up to cursor
const position = preCaretRange.toString().length; const position = preCaretRange.toString().length;
console.log('HeadingBlockHandler.getCursorPosition: Calculated position:', {
position,
preCaretText: preCaretRange.toString(),
elementText: headingBlock.textContent,
elementTextLength: headingBlock.textContent?.length
});
return position; return position;
} }
@ -363,7 +326,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// For headings, get the innerHTML which includes formatting tags // For headings, get the innerHTML which includes formatting tags
const content = headingBlock.innerHTML || ''; const content = headingBlock.innerHTML || '';
console.log('HeadingBlockHandler.getContent:', content);
return content; return content;
} }
@ -482,20 +444,11 @@ export class HeadingBlockHandler extends BaseBlockHandler {
} }
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null { getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
console.log('HeadingBlockHandler.getSplitContent: Starting...');
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
if (!headingBlock) { if (!headingBlock) {
console.log('HeadingBlockHandler.getSplitContent: No heading element found');
return null; return null;
} }
console.log('HeadingBlockHandler.getSplitContent: Element info:', {
innerHTML: headingBlock.innerHTML,
textContent: headingBlock.textContent,
textLength: headingBlock.textContent?.length
});
// Get shadow roots from context // Get shadow roots from context
const wysiwygBlock = context?.component; const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
@ -508,23 +461,12 @@ export class HeadingBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('HeadingBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('HeadingBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = headingBlock.textContent || ''; const fullText = headingBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length); const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
console.log('HeadingBlockHandler.getSplitContent: Splitting with last known position:', {
pos,
fullTextLength: fullText.length,
before: fullText.substring(0, pos),
after: fullText.substring(pos)
});
return { return {
before: fullText.substring(0, pos), before: fullText.substring(0, pos),
after: fullText.substring(pos) after: fullText.substring(pos)
@ -533,15 +475,8 @@ export class HeadingBlockHandler extends BaseBlockHandler {
return null; return null;
} }
console.log('HeadingBlockHandler.getSplitContent: Selection range:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
startContainerInElement: headingBlock.contains(selectionInfo.startContainer)
});
// Make sure the selection is within this block // Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) {
console.log('HeadingBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = headingBlock.textContent || ''; const fullText = headingBlock.textContent || '';
@ -556,11 +491,9 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Get cursor position first // Get cursor position first
const cursorPos = this.getCursorPosition(element, context); const cursorPos = this.getCursorPosition(element, context);
console.log('HeadingBlockHandler.getSplitContent: Cursor position for HTML split:', cursorPos);
if (cursorPos === null || cursorPos === 0) { if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content // If cursor is at start or can't determine position, move all content
console.log('HeadingBlockHandler.getSplitContent: Cursor at start or null, moving all content');
return { return {
before: '', before: '',
after: headingBlock.innerHTML after: headingBlock.innerHTML
@ -592,16 +525,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
tempDiv.appendChild(afterFragment); tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML; const afterHtml = tempDiv.innerHTML;
console.log('HeadingBlockHandler.getSplitContent: Final split result:', {
cursorPos,
beforeHtml,
beforeLength: beforeHtml.length,
beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''),
afterHtml,
afterLength: afterHtml.length,
afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '')
});
return { return {
before: beforeHtml, before: beforeHtml,
after: afterHtml after: afterHtml

View File

@ -17,8 +17,6 @@ export class ListBlockHandler extends BaseBlockHandler {
const listType = block.metadata?.listType || 'unordered'; const listType = block.metadata?.listType || 'unordered';
const listTag = listType === 'ordered' ? 'ol' : 'ul'; const listTag = listType === 'ordered' ? 'ol' : 'ul';
console.log('ListBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, listType });
// Render list content // Render list content
const listContent = this.renderListContent(block.content, block.metadata); const listContent = this.renderListContent(block.content, block.metadata);
@ -55,8 +53,6 @@ export class ListBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('ListBlockHandler.setup: Setting up list block', { blockId: block.id });
// Set initial content if needed // Set initial content if needed
if (block.content && !listBlock.innerHTML) { if (block.content && !listBlock.innerHTML) {
listBlock.innerHTML = this.renderListContent(block.content, block.metadata); listBlock.innerHTML = this.renderListContent(block.content, block.metadata);
@ -64,7 +60,6 @@ export class ListBlockHandler extends BaseBlockHandler {
// Input handler // Input handler
listBlock.addEventListener('input', (e) => { listBlock.addEventListener('input', (e) => {
console.log('ListBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
@ -104,24 +99,20 @@ export class ListBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
listBlock.addEventListener('focus', () => { listBlock.addEventListener('focus', () => {
console.log('ListBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
listBlock.addEventListener('blur', () => { listBlock.addEventListener('blur', () => {
console.log('ListBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
listBlock.addEventListener('compositionstart', () => { listBlock.addEventListener('compositionstart', () => {
console.log('ListBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
listBlock.addEventListener('compositionend', () => { listBlock.addEventListener('compositionend', () => {
console.log('ListBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -311,7 +302,6 @@ export class ListBlockHandler extends BaseBlockHandler {
.map(li => li.textContent || '') .map(li => li.textContent || '')
.join('\n'); .join('\n');
console.log('ListBlockHandler.getContent:', content);
return content; return content;
} }

View File

@ -16,7 +16,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const selectedClass = isSelected ? ' selected' : ''; const selectedClass = isSelected ? ' selected' : '';
const placeholder = this.getPlaceholder(); const placeholder = this.getPlaceholder();
console.log('ParagraphBlockHandler.render:', { blockId: block.id, isSelected, content: block.content });
return ` return `
<div <div
@ -36,7 +35,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('ParagraphBlockHandler.setup: Setting up paragraph block', { blockId: block.id });
// Set initial content if needed // Set initial content if needed
if (block.content && !paragraphBlock.innerHTML) { if (block.content && !paragraphBlock.innerHTML) {
@ -45,14 +43,12 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Input handler with cursor tracking // Input handler with cursor tracking
paragraphBlock.addEventListener('input', (e) => { paragraphBlock.addEventListener('input', (e) => {
console.log('ParagraphBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Updated cursor position after input', { pos });
} }
}); });
@ -62,7 +58,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position before keydown', { pos, key: e.key });
} }
handlers.onKeyDown(e); handlers.onKeyDown(e);
@ -70,24 +65,20 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
paragraphBlock.addEventListener('focus', () => { paragraphBlock.addEventListener('focus', () => {
console.log('ParagraphBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
paragraphBlock.addEventListener('blur', () => { paragraphBlock.addEventListener('blur', () => {
console.log('ParagraphBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
paragraphBlock.addEventListener('compositionstart', () => { paragraphBlock.addEventListener('compositionstart', () => {
console.log('ParagraphBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
paragraphBlock.addEventListener('compositionend', () => { paragraphBlock.addEventListener('compositionend', () => {
console.log('ParagraphBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -96,8 +87,7 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position after mouseup', { pos }); }
}
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
handlers.onMouseUp?.(e); handlers.onMouseUp?.(e);
@ -110,7 +100,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position after click', { pos });
} }
}, 0); }, 0);
}); });
@ -120,7 +109,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position after keyup', { pos, key: e.key });
} }
}); });
@ -171,11 +159,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('ParagraphBlockHandler: Text selected', {
text: selectedText,
blockId: block.id
});
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
@ -265,14 +248,9 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Helper methods for paragraph functionality // Helper methods for paragraph functionality
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
console.log('ParagraphBlockHandler.getCursorPosition: Called with element:', element, 'context:', context);
// Get the actual paragraph element // Get the actual paragraph element
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
if (!paragraphBlock) { if (!paragraphBlock) {
console.log('ParagraphBlockHandler.getCursorPosition: No paragraph element found');
console.log('Element innerHTML:', element.innerHTML);
console.log('Element tagName:', element.tagName);
return null; return null;
} }
@ -288,27 +266,12 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('ParagraphBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length,
element: element,
paragraphBlock: paragraphBlock
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('ParagraphBlockHandler.getCursorPosition: No selection found');
return null; return null;
} }
console.log('ParagraphBlockHandler.getCursorPosition: Range info:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed,
startContainerText: selectionInfo.startContainer.textContent
});
if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) {
console.log('ParagraphBlockHandler.getCursorPosition: Range not in element');
return null; return null;
} }
@ -319,12 +282,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Get the text content length up to cursor // Get the text content length up to cursor
const position = preCaretRange.toString().length; const position = preCaretRange.toString().length;
console.log('ParagraphBlockHandler.getCursorPosition: Calculated position:', {
position,
preCaretText: preCaretRange.toString(),
elementText: paragraphBlock.textContent,
elementTextLength: paragraphBlock.textContent?.length
});
return position; return position;
} }
@ -335,7 +292,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// For paragraphs, get the innerHTML which includes formatting tags // For paragraphs, get the innerHTML which includes formatting tags
const content = paragraphBlock.innerHTML || ''; const content = paragraphBlock.innerHTML || '';
console.log('ParagraphBlockHandler.getContent:', content);
return content; return content;
} }
@ -454,20 +410,11 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
} }
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null { getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
console.log('ParagraphBlockHandler.getSplitContent: Starting...');
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
if (!paragraphBlock) { if (!paragraphBlock) {
console.log('ParagraphBlockHandler.getSplitContent: No paragraph element found');
return null; return null;
} }
console.log('ParagraphBlockHandler.getSplitContent: Element info:', {
innerHTML: paragraphBlock.innerHTML,
textContent: paragraphBlock.textContent,
textLength: paragraphBlock.textContent?.length
});
// Get shadow roots from context // Get shadow roots from context
const wysiwygBlock = context?.component; const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
@ -480,23 +427,12 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('ParagraphBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('ParagraphBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = paragraphBlock.textContent || ''; const fullText = paragraphBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length); const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
console.log('ParagraphBlockHandler.getSplitContent: Splitting with last known position:', {
pos,
fullTextLength: fullText.length,
before: fullText.substring(0, pos),
after: fullText.substring(pos)
});
return { return {
before: fullText.substring(0, pos), before: fullText.substring(0, pos),
after: fullText.substring(pos) after: fullText.substring(pos)
@ -505,15 +441,8 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
return null; return null;
} }
console.log('ParagraphBlockHandler.getSplitContent: Selection range:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
startContainerInElement: paragraphBlock.contains(selectionInfo.startContainer)
});
// Make sure the selection is within this block // Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) {
console.log('ParagraphBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = paragraphBlock.textContent || ''; const fullText = paragraphBlock.textContent || '';
@ -528,11 +457,9 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Get cursor position first // Get cursor position first
const cursorPos = this.getCursorPosition(element, context); const cursorPos = this.getCursorPosition(element, context);
console.log('ParagraphBlockHandler.getSplitContent: Cursor position for HTML split:', cursorPos);
if (cursorPos === null || cursorPos === 0) { if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content // If cursor is at start or can't determine position, move all content
console.log('ParagraphBlockHandler.getSplitContent: Cursor at start or null, moving all content');
return { return {
before: '', before: '',
after: paragraphBlock.innerHTML after: paragraphBlock.innerHTML
@ -564,16 +491,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
tempDiv.appendChild(afterFragment); tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML; const afterHtml = tempDiv.innerHTML;
console.log('ParagraphBlockHandler.getSplitContent: Final split result:', {
cursorPos,
beforeHtml,
beforeLength: beforeHtml.length,
beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''),
afterHtml,
afterLength: afterHtml.length,
afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '')
});
return { return {
before: beforeHtml, before: beforeHtml,
after: afterHtml after: afterHtml

View File

@ -16,7 +16,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const selectedClass = isSelected ? ' selected' : ''; const selectedClass = isSelected ? ' selected' : '';
const placeholder = this.getPlaceholder(); const placeholder = this.getPlaceholder();
console.log('QuoteBlockHandler.render:', { blockId: block.id, isSelected, content: block.content });
return ` return `
<div <div
@ -36,8 +35,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('QuoteBlockHandler.setup: Setting up quote block', { blockId: block.id });
// Set initial content if needed // Set initial content if needed
if (block.content && !quoteBlock.innerHTML) { if (block.content && !quoteBlock.innerHTML) {
quoteBlock.innerHTML = block.content; quoteBlock.innerHTML = block.content;
@ -45,14 +42,12 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Input handler with cursor tracking // Input handler with cursor tracking
quoteBlock.addEventListener('input', (e) => { quoteBlock.addEventListener('input', (e) => {
console.log('QuoteBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Updated cursor position after input', { pos });
} }
}); });
@ -62,7 +57,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position before keydown', { pos, key: e.key });
} }
handlers.onKeyDown(e); handlers.onKeyDown(e);
@ -70,24 +64,20 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
quoteBlock.addEventListener('focus', () => { quoteBlock.addEventListener('focus', () => {
console.log('QuoteBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
quoteBlock.addEventListener('blur', () => { quoteBlock.addEventListener('blur', () => {
console.log('QuoteBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
quoteBlock.addEventListener('compositionstart', () => { quoteBlock.addEventListener('compositionstart', () => {
console.log('QuoteBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
quoteBlock.addEventListener('compositionend', () => { quoteBlock.addEventListener('compositionend', () => {
console.log('QuoteBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -96,7 +86,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position after mouseup', { pos });
} }
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
@ -110,7 +99,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position after click', { pos });
} }
}, 0); }, 0);
}); });
@ -120,7 +108,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position after keyup', { pos, key: e.key });
} }
}); });
@ -171,11 +158,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('QuoteBlockHandler: Text selected', {
text: selectedText,
blockId: block.id
});
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
@ -252,14 +234,9 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Helper methods for quote functionality // Helper methods for quote functionality
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
console.log('QuoteBlockHandler.getCursorPosition: Called with element:', element, 'context:', context);
// Get the actual quote element // Get the actual quote element
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement; const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
if (!quoteBlock) { if (!quoteBlock) {
console.log('QuoteBlockHandler.getCursorPosition: No quote element found');
console.log('Element innerHTML:', element.innerHTML);
console.log('Element tagName:', element.tagName);
return null; return null;
} }
@ -275,27 +252,12 @@ export class QuoteBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('QuoteBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length,
element: element,
quoteBlock: quoteBlock
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('QuoteBlockHandler.getCursorPosition: No selection found');
return null; return null;
} }
console.log('QuoteBlockHandler.getCursorPosition: Range info:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed,
startContainerText: selectionInfo.startContainer.textContent
});
if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) {
console.log('QuoteBlockHandler.getCursorPosition: Range not in element');
return null; return null;
} }
@ -306,12 +268,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Get the text content length up to cursor // Get the text content length up to cursor
const position = preCaretRange.toString().length; const position = preCaretRange.toString().length;
console.log('QuoteBlockHandler.getCursorPosition: Calculated position:', {
position,
preCaretText: preCaretRange.toString(),
elementText: quoteBlock.textContent,
elementTextLength: quoteBlock.textContent?.length
});
return position; return position;
} }
@ -322,7 +278,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// For quotes, get the innerHTML which includes formatting tags // For quotes, get the innerHTML which includes formatting tags
const content = quoteBlock.innerHTML || ''; const content = quoteBlock.innerHTML || '';
console.log('QuoteBlockHandler.getContent:', content);
return content; return content;
} }
@ -413,20 +368,11 @@ export class QuoteBlockHandler extends BaseBlockHandler {
} }
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null { getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
console.log('QuoteBlockHandler.getSplitContent: Starting...');
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement; const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
if (!quoteBlock) { if (!quoteBlock) {
console.log('QuoteBlockHandler.getSplitContent: No quote element found');
return null; return null;
} }
console.log('QuoteBlockHandler.getSplitContent: Element info:', {
innerHTML: quoteBlock.innerHTML,
textContent: quoteBlock.textContent,
textLength: quoteBlock.textContent?.length
});
// Get shadow roots from context // Get shadow roots from context
const wysiwygBlock = context?.component; const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
@ -439,23 +385,12 @@ export class QuoteBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('QuoteBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('QuoteBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = quoteBlock.textContent || ''; const fullText = quoteBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length); const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
console.log('QuoteBlockHandler.getSplitContent: Splitting with last known position:', {
pos,
fullTextLength: fullText.length,
before: fullText.substring(0, pos),
after: fullText.substring(pos)
});
return { return {
before: fullText.substring(0, pos), before: fullText.substring(0, pos),
after: fullText.substring(pos) after: fullText.substring(pos)
@ -464,15 +399,8 @@ export class QuoteBlockHandler extends BaseBlockHandler {
return null; return null;
} }
console.log('QuoteBlockHandler.getSplitContent: Selection range:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
startContainerInElement: quoteBlock.contains(selectionInfo.startContainer)
});
// Make sure the selection is within this block // Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) {
console.log('QuoteBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = quoteBlock.textContent || ''; const fullText = quoteBlock.textContent || '';
@ -487,11 +415,9 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Get cursor position first // Get cursor position first
const cursorPos = this.getCursorPosition(element, context); const cursorPos = this.getCursorPosition(element, context);
console.log('QuoteBlockHandler.getSplitContent: Cursor position for HTML split:', cursorPos);
if (cursorPos === null || cursorPos === 0) { if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content // If cursor is at start or can't determine position, move all content
console.log('QuoteBlockHandler.getSplitContent: Cursor at start or null, moving all content');
return { return {
before: '', before: '',
after: quoteBlock.innerHTML after: quoteBlock.innerHTML
@ -523,16 +449,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
tempDiv.appendChild(afterFragment); tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML; const afterHtml = tempDiv.innerHTML;
console.log('QuoteBlockHandler.getSplitContent: Final split result:', {
cursorPos,
beforeHtml,
beforeLength: beforeHtml.length,
beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''),
afterHtml,
afterLength: afterHtml.length,
afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '')
});
return { return {
before: beforeHtml, before: beforeHtml,
after: afterHtml after: afterHtml

View File

@ -7,6 +7,7 @@ import {
css, css,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexLayers } from '../00zindex.js';
import { WysiwygFormatting } from './wysiwyg.formatting.js'; import { WysiwygFormatting } from './wysiwyg.formatting.js';
@ -41,7 +42,7 @@ export class DeesFormattingMenu extends DeesElement {
css` css`
:host { :host {
position: fixed; position: fixed;
z-index: 10000; z-index: ${zIndexLayers.wysiwygMenus};
pointer-events: none; pointer-events: none;
} }

View File

@ -243,6 +243,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
onCompositionStart: () => this.isComposing = true, onCompositionStart: () => this.isComposing = true,
onCompositionEnd: () => this.isComposing = false, onCompositionEnd: () => this.isComposing = false,
onMouseUp: (e: MouseEvent) => this.handleTextSelection(e), onMouseUp: (e: MouseEvent) => this.handleTextSelection(e),
onRequestUpdate: () => this.updateBlockElement(block.id),
}; };
wrapper.appendChild(blockComponent); wrapper.appendChild(blockComponent);

View File

@ -8,6 +8,7 @@ import {
css, css,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { zIndexLayers } from '../00zindex.js';
import { type ISlashMenuItem } from './wysiwyg.types.js'; import { type ISlashMenuItem } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
@ -49,7 +50,7 @@ export class DeesSlashMenu extends DeesElement {
css` css`
:host { :host {
position: fixed; position: fixed;
z-index: 10000; z-index: ${zIndexLayers.wysiwygMenus};
pointer-events: none; pointer-events: none;
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,14 +10,26 @@
* to the new block handler architecture using a unified HeadingBlockHandler. * to the new block handler architecture using a unified HeadingBlockHandler.
* Phase 5 Complete: Quote, Code, and List blocks have been successfully migrated * Phase 5 Complete: Quote, Code, and List blocks have been successfully migrated
* to the new block handler architecture. * to the new block handler architecture.
* Phase 6 Complete: Image, YouTube, and Attachment blocks have been successfully migrated
* to the new block handler architecture.
* Phase 7 Complete: Markdown and HTML blocks have been successfully migrated
* to the new block handler architecture.
*/ */
import { BlockRegistry, DividerBlockHandler } from './blocks/index.js'; import {
import { ParagraphBlockHandler } from './blocks/text/paragraph.block.js'; BlockRegistry,
import { HeadingBlockHandler } from './blocks/text/heading.block.js'; DividerBlockHandler,
import { QuoteBlockHandler } from './blocks/text/quote.block.js'; ParagraphBlockHandler,
import { CodeBlockHandler } from './blocks/text/code.block.js'; HeadingBlockHandler,
import { ListBlockHandler } from './blocks/text/list.block.js'; QuoteBlockHandler,
CodeBlockHandler,
ListBlockHandler,
ImageBlockHandler,
YouTubeBlockHandler,
AttachmentBlockHandler,
MarkdownBlockHandler,
HtmlBlockHandler
} from './blocks/index.js';
// Initialize and register all block handlers // Initialize and register all block handlers
export function registerAllBlockHandlers(): void { export function registerAllBlockHandlers(): void {
@ -33,14 +45,14 @@ export function registerAllBlockHandlers(): void {
BlockRegistry.register('code', new CodeBlockHandler()); BlockRegistry.register('code', new CodeBlockHandler());
BlockRegistry.register('list', new ListBlockHandler()); BlockRegistry.register('list', new ListBlockHandler());
// TODO: Register media blocks when implemented // Register media blocks
// BlockRegistry.register('image', new ImageBlockHandler()); BlockRegistry.register('image', new ImageBlockHandler());
// BlockRegistry.register('youtube', new YoutubeBlockHandler()); BlockRegistry.register('youtube', new YouTubeBlockHandler());
// BlockRegistry.register('attachment', new AttachmentBlockHandler()); BlockRegistry.register('attachment', new AttachmentBlockHandler());
// TODO: Register other content blocks when implemented // Register other content blocks
// BlockRegistry.register('markdown', new MarkdownBlockHandler()); BlockRegistry.register('markdown', new MarkdownBlockHandler());
// BlockRegistry.register('html', new HtmlBlockHandler()); BlockRegistry.register('html', new HtmlBlockHandler());
} }
// Ensure blocks are registered when this module is imported // Ensure blocks are registered when this module is imported

View File

@ -85,4 +85,5 @@ export interface IBlockEventHandlers {
onCompositionStart: () => void; onCompositionStart: () => void;
onCompositionEnd: () => void; onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void; onMouseUp?: (e: MouseEvent) => void;
onRequestUpdate?: () => void; // Request immediate re-render of the block
} }

View File

@ -93,20 +93,9 @@ export class WysiwygKeyboardHandler {
*/ */
private handleTab(e: KeyboardEvent, block: IBlock): void { private handleTab(e: KeyboardEvent, block: IBlock): void {
if (block.type === 'code') { if (block.type === 'code') {
// Allow tab in code blocks // Allow tab in code blocks - handled by CodeBlockHandler
e.preventDefault(); // Let it bubble to the block handler
// Insert two spaces for tab return;
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(' ');
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
}
} else if (block.type === 'list') { } else if (block.type === 'list') {
// Future: implement list indentation // Future: implement list indentation
e.preventDefault(); e.preventDefault();
@ -120,7 +109,8 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// For non-editable blocks, create a new paragraph after // For non-editable blocks, create a new paragraph after
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const newBlock = blockOps.createBlock(); const newBlock = blockOps.createBlock();
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
@ -145,59 +135,33 @@ export class WysiwygKeyboardHandler {
// Split content at cursor position // Split content at cursor position
e.preventDefault(); e.preventDefault();
console.log('Enter key pressed in block:', {
blockId: block.id,
blockType: block.type,
blockContent: block.content,
blockContentLength: block.content?.length || 0,
eventTarget: e.target,
eventTargetTagName: (e.target as HTMLElement).tagName
});
// Get the block component - need to search in the wysiwyg component's shadow DOM // Get the block component - need to search in the wysiwyg component's shadow DOM
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
console.log('Found block wrapper:', blockWrapper);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any; const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
console.log('Found block component:', blockComponent, 'has getSplitContent:', !!blockComponent?.getSplitContent);
if (blockComponent && blockComponent.getSplitContent) { if (blockComponent && blockComponent.getSplitContent) {
console.log('Calling getSplitContent...');
const splitContent = blockComponent.getSplitContent(); const splitContent = blockComponent.getSplitContent();
console.log('Enter key split content result:', {
hasSplitContent: !!splitContent,
beforeLength: splitContent?.before?.length || 0,
afterLength: splitContent?.after?.length || 0,
splitContent
});
if (splitContent) { if (splitContent) {
console.log('Updating current block with before content...');
// Update current block with content before cursor // Update current block with content before cursor
blockComponent.setContent(splitContent.before); blockComponent.setContent(splitContent.before);
block.content = splitContent.before; block.content = splitContent.before;
console.log('Creating new block with after content...');
// Create new block with content after cursor // Create new block with content after cursor
const newBlock = blockOps.createBlock('paragraph', splitContent.after); const newBlock = blockOps.createBlock('paragraph', splitContent.after);
console.log('Inserting new block...');
// Insert the new block // Insert the new block
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
// Update the value after both blocks are set // Update the value after both blocks are set
this.component.updateValue(); this.component.updateValue();
console.log('Enter key handling complete');
} else { } else {
// Fallback - just create empty block // Fallback - just create empty block
console.log('No split content returned, creating empty block');
const newBlock = blockOps.createBlock(); const newBlock = blockOps.createBlock();
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
} }
} else { } else {
// No block component or method, just create empty block // No block component or method, just create empty block
console.log('No getSplitContent method, creating empty block');
const newBlock = blockOps.createBlock(); const newBlock = blockOps.createBlock();
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
} }
@ -234,7 +198,7 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// Handle non-editable blocks // Handle non-editable blocks
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
@ -294,7 +258,7 @@ export class WysiwygKeyboardHandler {
// Get the actual editable element // Get the actual editable element
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -315,7 +279,7 @@ export class WysiwygKeyboardHandler {
if (prevBlock) { if (prevBlock) {
// If previous block is non-editable, select it first // If previous block is non-editable, select it first
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(prevBlock.type)) { if (nonEditableTypes.includes(prevBlock.type)) {
await blockOps.focusBlock(prevBlock.id); await blockOps.focusBlock(prevBlock.id);
return; return;
@ -407,7 +371,7 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// Handle non-editable blocks - same as backspace // Handle non-editable blocks - same as backspace
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
@ -445,7 +409,7 @@ export class WysiwygKeyboardHandler {
blockOps.removeBlock(block.id); blockOps.removeBlock(block.id);
// Focus the appropriate block // Focus the appropriate block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nextBlock && !nonEditableTypes.includes(nextBlock.type)) { if (nextBlock && !nonEditableTypes.includes(nextBlock.type)) {
await blockOps.focusBlock(nextBlock.id, 'start'); await blockOps.focusBlock(nextBlock.id, 'start');
} else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) { } else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) {
@ -468,7 +432,7 @@ export class WysiwygKeyboardHandler {
// Get the actual editable element // Get the actual editable element
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -485,7 +449,7 @@ export class WysiwygKeyboardHandler {
if (cursorPos === textLength) { if (cursorPos === textLength) {
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nextBlock && nonEditableTypes.includes(nextBlock.type)) { if (nextBlock && nonEditableTypes.includes(nextBlock.type)) {
e.preventDefault(); e.preventDefault();
await blockOps.focusBlock(nextBlock.id); await blockOps.focusBlock(nextBlock.id);
@ -501,7 +465,7 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, always navigate to previous block // For non-editable blocks, always navigate to previous block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
@ -518,9 +482,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -540,7 +504,7 @@ export class WysiwygKeyboardHandler {
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
} }
@ -552,14 +516,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, always navigate to next block // For non-editable blocks, always navigate to next block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
return; return;
@ -570,9 +534,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -592,7 +556,7 @@ export class WysiwygKeyboardHandler {
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
} }
@ -620,14 +584,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, navigate to previous block // For non-editable blocks, navigate to previous block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
return; return;
@ -638,9 +602,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -662,7 +626,7 @@ export class WysiwygKeyboardHandler {
if (prevBlock) { if (prevBlock) {
e.preventDefault(); e.preventDefault();
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
const position = nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'; const position = nonEditableTypes.includes(prevBlock.type) ? undefined : 'end';
await blockOps.focusBlock(prevBlock.id, position); await blockOps.focusBlock(prevBlock.id, position);
} }
@ -675,14 +639,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, navigate to next block // For non-editable blocks, navigate to next block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
return; return;
@ -693,9 +657,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -718,7 +682,7 @@ export class WysiwygKeyboardHandler {
if (nextBlock) { if (nextBlock) {
e.preventDefault(); e.preventDefault();
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
} }

View File

@ -1,4 +1,4 @@
import { html, type TemplateResult } from '@design.estate/dees-element'; import { html, type TemplateResult, cssManager } from '@design.estate/dees-element';
import { DeesModal } from '../dees-modal.js'; import { DeesModal } from '../dees-modal.js';
import { type IBlock } from './wysiwyg.types.js'; import { type IBlock } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
@ -16,37 +16,56 @@ export class WysiwygModalManager {
heading: 'Select Programming Language', heading: 'Select Programming Language',
content: html` content: html`
<style> <style>
.language-container {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.language-grid { .language-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px; gap: 8px;
padding: 16px;
} }
.language-button { .language-button {
padding: 12px; padding: 12px 8px;
background: var(--dees-color-box); background: transparent;
border: 1px solid var(--dees-color-line-bright); border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
transition: all 0.2s; font-size: 13px;
font-weight: 500;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
} }
.language-button:hover { .language-button:hover {
background: var(--dees-color-box-highlight); background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: var(--dees-color-primary); border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
.language-button.selected {
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
} }
</style> </style>
<div class="language-grid"> <div class="language-container">
${this.getLanguages().map(lang => html` <div class="language-grid">
<div class="language-button" @click="${(e: MouseEvent) => { ${this.getLanguages().map(lang => html`
selectedLanguage = lang.toLowerCase(); <div
const modal = (e.target as HTMLElement).closest('dees-modal'); class="language-button ${selectedLanguage === lang.toLowerCase() ? 'selected' : ''}"
if (modal) { @click="${() => {
const okButton = modal.shadowRoot?.querySelector('.bottomButton.ok') as HTMLElement; selectedLanguage = lang.toLowerCase();
if (okButton) okButton.click(); // Close modal by finding it in DOM
} const modal = document.querySelector('dees-modal');
}}">${lang}</div> if (modal && typeof (modal as any).destroy === 'function') {
`)} (modal as any).destroy();
}
resolve(selectedLanguage);
}}">
${lang}
</div>
`)}
</div>
</div> </div>
`, `,
menuOptions: [ menuOptions: [
@ -56,13 +75,6 @@ export class WysiwygModalManager {
modal.destroy(); modal.destroy();
resolve(null); resolve(null);
} }
},
{
name: 'OK',
action: async (modal) => {
modal.destroy();
resolve(selectedLanguage);
}
} }
] ]
}); });
@ -76,48 +88,61 @@ export class WysiwygModalManager {
block: IBlock, block: IBlock,
onUpdate: (block: IBlock) => void onUpdate: (block: IBlock) => void
): Promise<void> { ): Promise<void> {
const content = html` const content = html`
<style> <style>
.settings-container { .settings-container {
padding: 16px; padding: 16px;
} }
.settings-section { .settings-section {
margin-bottom: 20px; margin-bottom: 24px;
}
.settings-section:last-child {
margin-bottom: 0;
} }
.settings-label { .settings-label {
font-weight: 500; font-weight: 500;
margin-bottom: 8px; margin-bottom: 8px;
color: var(--dees-color-text); color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
} }
.block-type-grid { .block-type-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 8px; gap: 8px;
margin-bottom: 16px;
} }
.block-type-button { .block-type-button {
padding: 12px; padding: 12px;
background: var(--dees-color-box); background: transparent;
border: 1px solid var(--dees-color-line-bright); border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: left;
transition: all 0.2s; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 13px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
} }
.block-type-button:hover { .block-type-button:hover {
background: var(--dees-color-box-highlight); background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: var(--dees-color-primary); border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
} }
.block-type-button.selected { .block-type-button.selected {
background: var(--dees-color-primary); background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
color: white; border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
} }
.block-type-icon { .block-type-icon {
font-weight: 600; font-weight: 500;
font-size: 16px; font-size: 16px;
width: 20px;
text-align: center;
flex-shrink: 0;
opacity: 0.7;
} }
</style> </style>
<div class="settings-container"> <div class="settings-container">
@ -131,7 +156,7 @@ export class WysiwygModalManager {
content, content,
menuOptions: [ menuOptions: [
{ {
name: 'Close', name: 'Done',
action: async (modal) => { action: async (modal) => {
modal.destroy(); modal.destroy();
} }
@ -147,57 +172,55 @@ export class WysiwygModalManager {
block: IBlock, block: IBlock,
onUpdate: (block: IBlock) => void onUpdate: (block: IBlock) => void
): TemplateResult { ): TemplateResult {
const currentLanguage = block.metadata?.language || 'plain text'; const currentLanguage = block.metadata?.language || 'javascript';
return html` return html`
<style> <style>
.settings-section {
margin-bottom: 16px;
}
.settings-label {
font-weight: 500;
margin-bottom: 8px;
}
.language-grid { .language-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px; gap: 6px;
} }
.language-button { .language-button {
padding: 8px; padding: 8px 4px;
background: var(--dees-color-box); background: transparent;
border: 1px solid var(--dees-color-line-bright); border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
transition: all 0.2s; transition: all 0.15s ease;
font-size: 12px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
} }
.language-button:hover { .language-button:hover {
background: var(--dees-color-box-highlight); background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: var(--dees-color-primary); border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
} }
.language-button.selected { .language-button.selected {
background: var(--dees-color-primary); background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
color: white; border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
} }
</style> </style>
<div class="settings-section"> <div class="settings-section">
<div class="settings-label">Programming Language</div> <div class="settings-label">Programming Language</div>
<div class="language-grid"> <div class="language-grid">
${this.getLanguages().map(lang => html` ${this.getLanguages().map(lang => html`
<div class="language-button ${currentLanguage === lang.toLowerCase() ? 'selected' : ''}" <div
@click="${(e: MouseEvent) => { class="language-button ${currentLanguage === lang.toLowerCase() ? 'selected' : ''}"
@click="${() => {
if (!block.metadata) block.metadata = {}; if (!block.metadata) block.metadata = {};
block.metadata.language = lang.toLowerCase(); block.metadata.language = lang.toLowerCase();
onUpdate(block); onUpdate(block);
// Close modal // Close modal immediately
const modal = (e.target as HTMLElement).closest('dees-modal'); const modal = document.querySelector('dees-modal');
if (modal) { if (modal && typeof (modal as any).destroy === 'function') {
const closeButton = modal.shadowRoot?.querySelector('.bottomButton') as HTMLElement; (modal as any).destroy();
if (closeButton) closeButton.click();
} }
}}">${lang}</div> }}"
data-lang="${lang}"
>${lang}</div>
`)} `)}
</div> </div>
</div> </div>
@ -228,6 +251,8 @@ export class WysiwygModalManager {
<div <div
class="block-type-button ${block.type === item.type ? 'selected' : ''}" class="block-type-button ${block.type === item.type ? 'selected' : ''}"
@click="${async (e: MouseEvent) => { @click="${async (e: MouseEvent) => {
const button = e.currentTarget as HTMLElement;
const oldType = block.type; const oldType = block.type;
block.type = item.type as IBlock['type']; block.type = item.type as IBlock['type'];
@ -252,11 +277,10 @@ export class WysiwygModalManager {
onUpdate(block); onUpdate(block);
// Close modal after selection // Close modal immediately
const modal = (e.target as HTMLElement).closest('dees-modal'); const modal = document.querySelector('dees-modal');
if (modal) { if (modal && typeof (modal as any).destroy === 'function') {
const closeButton = modal.shadowRoot?.querySelector('.bottomButton') as HTMLElement; (modal as any).destroy();
if (closeButton) closeButton.click();
} }
}}" }}"
> >

3
ts_web/pages/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './mainpage.js';
export * from './input-showcase.js';
export * from './zindex-showcase.js';

View File

@ -0,0 +1,617 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import '../elements/index.js';
export const inputShowcase = () => html`
<div class="page-wrapper">
<style>
${css`
.page-wrapper {
display: block;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
min-height: 100%;
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
}
.showcase-container {
max-width: 1200px;
margin: 0 auto;
padding: 48px 24px;
}
.showcase-header {
text-align: center;
margin-bottom: 48px;
}
.showcase-title {
font-size: 48px;
font-weight: 700;
margin: 0 0 16px 0;
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.showcase-subtitle {
font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')};
margin: 0;
line-height: 1.6;
}
.showcase-section {
margin-bottom: 48px;
}
.showcase-section:last-child {
margin-bottom: 0;
padding-bottom: 48px;
}
/* Ensure all headings are theme-aware */
h1, h2, h3, h4, h5, h6 {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
margin: 0;
}
p {
color: ${cssManager.bdTheme('#666', '#999')};
}
strong {
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 32px;
}
.section-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
}
.section-icon.text { background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; }
.section-icon.selection { background: ${cssManager.bdTheme('#f3e5f5', '#4a148c')}; }
.section-icon.numeric { background: ${cssManager.bdTheme('#e8f5e9', '#1b5e20')}; }
.section-icon.special { background: ${cssManager.bdTheme('#fff3e0', '#e65100')}; }
.section-icon.rich { background: ${cssManager.bdTheme('#fce4ec', '#880e4f')}; }
.section-icon.form { background: ${cssManager.bdTheme('#e0f2f1', '#004d40')}; }
.section-title {
font-size: 32px;
font-weight: 600;
margin: 0;
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.section-description {
color: ${cssManager.bdTheme('#666', '#999')};
margin: 0 0 32px 64px;
font-size: 16px;
line-height: 1.6;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
@media (max-width: 900px) {
.demo-grid {
grid-template-columns: 1fr;
}
}
.nav-menu {
position: sticky;
top: 24px;
float: right;
margin-left: 24px;
margin-bottom: 24px;
background: ${cssManager.bdTheme('white', '#1a1a1a')};
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.5)')};
z-index: 100;
}
@media (max-width: 1200px) {
.nav-menu {
display: none;
}
}
.nav-item {
display: block;
padding: 8px 12px;
color: ${cssManager.bdTheme('#666', '#999')};
text-decoration: none;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s;
}
.nav-item:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
dees-form {
margin-top: 32px;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.code-example {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 8px;
padding: 16px;
margin-top: 16px;
font-family: 'Fira Code', monospace;
font-size: 14px;
overflow-x: auto;
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
}
.feature-badge {
display: inline-block;
padding: 4px 12px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
color: ${cssManager.bdTheme('#1976d2', '#64b5f6')};
border-radius: 16px;
font-size: 12px;
font-weight: 500;
margin-left: 8px;
}
/* Form section specific styles */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-top: 16px;
}
.feature-card {
padding: 16px;
border-radius: 8px;
}
.feature-card p {
margin: 8px 0 0 0;
font-size: 14px;
}
`}
</style>
<div class="showcase-container">
<!-- Navigation Menu -->
<nav class="nav-menu">
<a href="#text-inputs" class="nav-item">📝 Text Inputs</a>
<a href="#selection-inputs" class="nav-item">☑️ Selection Inputs</a>
<a href="#numeric-inputs" class="nav-item">🔢 Numeric Inputs</a>
<a href="#special-inputs" class="nav-item">✨ Special Inputs</a>
<a href="#rich-editors" class="nav-item">📄 Rich Editors</a>
<a href="#form-integration" class="nav-item">📋 Form Integration</a>
</nav>
<div class="showcase-header">
<h1 class="showcase-title">Input Components Showcase</h1>
<p class="showcase-subtitle">
A comprehensive collection of input components for building modern web forms and interfaces.
<br>All components support dark mode, validation, and integrate seamlessly with dees-form.
</p>
</div>
<!-- Text Inputs Section -->
<section id="text-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon text">📝</div>
<h2 class="section-title">Text Inputs</h2>
</div>
<p class="section-description">
Standard text input components for collecting various types of textual data.
Includes password fields, validation, and specialized formatting.
</p>
<dees-panel .title=${'Basic Text Inputs'}>
<div class="demo-grid">
<dees-input-text
.label=${'Username'}
.placeholder=${'Enter your username'}
.description=${'Choose a unique username'}
.required=${true}
></dees-input-text>
<dees-input-text
.label=${'Email Address'}
.inputType=${'email'}
.placeholder=${'user@example.com'}
.validationText=${'Please enter a valid email'}
.required=${true}
></dees-input-text>
<dees-input-text
.label=${'Password'}
.isPasswordBool=${true}
.placeholder=${'Enter secure password'}
.description=${'Must be at least 8 characters'}
></dees-input-text>
<dees-input-text
.label=${'Website URL'}
.inputType=${'url'}
.placeholder=${'https://example.com'}
.value=${'https://design.estate'}
></dees-input-text>
</div>
</dees-panel>
<dees-panel .title=${'Search Bar'} .subtitle=${'Advanced search with suggestions'}>
<dees-searchbar
.placeholder=${'Search for anything...'}
></dees-searchbar>
<div class="code-example">
// Search with custom suggestions
&lt;dees-searchbar
.placeholder="Search products..."
.suggestions=${['Laptop', 'Phone', 'Tablet']}
&gt;&lt;/dees-searchbar&gt;
</div>
</dees-panel>
</section>
<!-- Selection Inputs Section -->
<section id="selection-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon selection">☑️</div>
<h2 class="section-title">Selection Inputs</h2>
</div>
<p class="section-description">
Components for selecting from predefined options. Includes checkboxes, radio buttons,
dropdowns, and multi-select controls.
</p>
<dees-panel .title=${'Checkboxes and Radio Buttons'}>
<div class="demo-grid">
<div>
<dees-input-checkbox
.label=${'Accept Terms & Conditions'}
.description=${'You must accept to continue'}
.required=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Subscribe to Newsletter'}
.value=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Enable Notifications'}
.disabled=${true}
></dees-input-checkbox>
</div>
<div>
<dees-input-radiogroup
.label=${'Select Plan'}
.options=${['Free', 'Pro', 'Enterprise']}
.selectedOption=${'Pro'}
.required=${true}
></dees-input-radiogroup>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Dropdown Selection'}>
<div class="demo-grid">
<dees-input-dropdown
.label=${'Country'}
.options=${[
{option: 'United States', key: 'us', payload: 'US'},
{option: 'Canada', key: 'ca', payload: 'CA'},
{option: 'United Kingdom', key: 'uk', payload: 'UK'},
{option: 'Germany', key: 'de', payload: 'DE'},
{option: 'France', key: 'fr', payload: 'FR'},
{option: 'Japan', key: 'jp', payload: 'JP'}
]}
.placeholder=${'Select your country'}
.required=${true}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Preferred Language'}
.options=${[
{option: 'English', key: 'en', payload: 'EN'},
{option: 'Spanish', key: 'es', payload: 'ES'},
{option: 'French', key: 'fr', payload: 'FR'},
{option: 'German', key: 'de', payload: 'DE'},
{option: 'Japanese', key: 'ja', payload: 'JA'}
]}
.value=${{option: 'English', key: 'en', payload: 'EN'}}
></dees-input-dropdown>
</div>
</dees-panel>
<dees-panel .title=${'Multi Toggle'} .subtitle=${'Toggle between multiple options'}>
<dees-input-multitoggle
.label=${'Theme Preference'}
.options=${['Light', 'Dark', 'Auto']}
.value=${'Auto'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'View Mode'}
.options=${['Grid', 'List', 'Cards']}
.value=${'Grid'}
.description=${'Choose how to display items'}
></dees-input-multitoggle>
</dees-panel>
<dees-panel .title=${'Type List'} .subtitle=${'Dynamic list of typed items'}>
<dees-input-typelist
.label=${'Skills'}
.description=${'Add your technical skills'}
.placeholder=${'Type and press Enter'}
></dees-input-typelist>
</dees-panel>
<dees-panel .title=${'Tags Input'} .subtitle=${'Add and manage tags with suggestions'}>
<div class="demo-grid">
<dees-input-tags
.label=${'Project Tags'}
.placeholder=${'Add tags...'}
.value=${['frontend', 'typescript', 'webcomponents']}
.description=${'Press Enter or comma to add'}
></dees-input-tags>
<dees-input-tags
.label=${'Technologies'}
.placeholder=${'Type to see suggestions...'}
.suggestions=${[
'React', 'Vue', 'Angular', 'Svelte',
'Node.js', 'Deno', 'Express', 'MongoDB'
]}
.value=${['React', 'Node.js']}
.maxTags=${5}
.description=${'Maximum 5 tags, with suggestions'}
></dees-input-tags>
</div>
</dees-panel>
</section>
<!-- Numeric Inputs Section -->
<section id="numeric-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon numeric">🔢</div>
<h2 class="section-title">Numeric Inputs</h2>
</div>
<p class="section-description">
Specialized inputs for numeric values, including quantity selectors and formatted inputs.
</p>
<dees-panel .title=${'Quantity Selector'}>
<div class="demo-grid">
<dees-input-quantityselector
.label=${'Product Quantity'}
.value=${1}
.min=${1}
.max=${100}
.description=${'Select quantity (1-100)'}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Team Size'}
.value=${5}
.min=${1}
.max=${50}
.step=${5}
></dees-input-quantityselector>
</div>
</dees-panel>
</section>
<!-- Special Inputs Section -->
<section id="special-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon special">✨</div>
<h2 class="section-title">Special Inputs</h2>
</div>
<p class="section-description">
Specialized input components for specific data types like phone numbers, IBAN, and file uploads.
</p>
<dees-panel .title=${'Phone & IBAN'}>
<div class="demo-grid">
<dees-input-phone
.label=${'Phone Number'}
.placeholder=${'+1 (555) 123-4567'}
.required=${true}
.description=${'International format supported'}
></dees-input-phone>
<dees-input-iban
.label=${'Bank Account (IBAN)'}
.placeholder=${'DE89 3704 0044 0532 0130 00'}
.description=${'European IBAN format'}
></dees-input-iban>
</div>
</dees-panel>
<dees-panel .title=${'File Upload'} .subtitle=${'Drag & drop or click to upload'}>
<dees-input-fileupload
.label=${'Upload Documents'}
.description=${'PDF, DOC, DOCX up to 10MB'}
.accept=${'.pdf,.doc,.docx'}
.multiple=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Profile Picture'}
.description=${'JPG, PNG up to 5MB'}
.accept=${'image/*'}
></dees-input-fileupload>
</dees-panel>
</section>
<!-- Rich Editors Section -->
<section id="rich-editors" class="showcase-section">
<div class="section-header">
<div class="section-icon rich">📄</div>
<h2 class="section-title">Rich Text Editors</h2>
<span class="feature-badge">New!</span>
</div>
<p class="section-description">
Advanced text editors for creating rich content with formatting, images, and structured blocks.
</p>
<dees-panel .title=${'Rich Text Editor'} .subtitle=${'TipTap-based rich text editing'}>
<dees-input-richtext
.label=${'Article Content'}
.placeholder=${'Start writing...'}
.description=${'Full formatting toolbar with markdown shortcuts'}
.minHeight=${300}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'WYSIWYG Block Editor'} .subtitle=${'Block-based editor with slash commands'}>
<dees-input-wysiwyg
.label=${'Page Content'}
.description=${'Type "/" for commands or use markdown shortcuts'}
.outputFormat=${'html'}
></dees-input-wysiwyg>
</dees-panel>
</section>
<!-- Form Integration Section -->
<section id="form-integration" class="showcase-section">
<div class="section-header">
<div class="section-icon form">📋</div>
<h2 class="section-title">Form Integration</h2>
</div>
<p class="section-description">
All input components integrate seamlessly with dees-form for validation,
submission handling, and data management.
</p>
<dees-panel .title=${'Complete Form Example'} .subtitle=${'All inputs working together'}>
<dees-form>
<h3>User Registration</h3>
<div class="demo-grid">
<dees-input-text
.label=${'First Name'}
.required=${true}
.key=${'firstName'}
></dees-input-text>
<dees-input-text
.label=${'Last Name'}
.required=${true}
.key=${'lastName'}
></dees-input-text>
</div>
<dees-input-text
.label=${'Email'}
.inputType=${'email'}
.required=${true}
.key=${'email'}
></dees-input-text>
<dees-input-phone
.label=${'Phone Number'}
.required=${true}
.key=${'phone'}
></dees-input-phone>
<dees-input-dropdown
.label=${'Country'}
.options=${[
{option: 'United States', key: 'us', payload: 'US'},
{option: 'Canada', key: 'ca', payload: 'CA'},
{option: 'United Kingdom', key: 'uk', payload: 'UK'},
{option: 'Germany', key: 'de', payload: 'DE'},
{option: 'France', key: 'fr', payload: 'FR'}
]}
.required=${true}
.key=${'country'}
></dees-input-dropdown>
<dees-input-radiogroup
.label=${'Account Type'}
.options=${['Personal', 'Business']}
.required=${true}
.key=${'accountType'}
.selectedOption=${'Personal'}
></dees-input-radiogroup>
<dees-input-richtext
.label=${'Bio'}
.placeholder=${'Tell us about yourself...'}
.minHeight=${150}
.key=${'bio'}
></dees-input-richtext>
<dees-input-checkbox
.label=${'I agree to the Terms of Service'}
.required=${true}
.key=${'terms'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Subscribe to newsletter'}
.key=${'newsletter'}
></dees-input-checkbox>
<dees-form-submit .text=${'Create Account'}></dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'Form Features'}>
<div class="feature-grid">
<div class="feature-card" style="background: rgba(0, 150, 136, 0.1);">
<strong>✅ Validation</strong>
<p>Built-in validation for all input types</p>
</div>
<div class="feature-card" style="background: rgba(33, 150, 243, 0.1);">
<strong>🔄 Two-way Binding</strong>
<p>Automatic data synchronization</p>
</div>
<div class="feature-card" style="background: rgba(156, 39, 176, 0.1);">
<strong>📊 Data Collection</strong>
<p>Easy form data extraction</p>
</div>
<div class="feature-card" style="background: rgba(255, 152, 0, 0.1);">
<strong>🎨 Theming</strong>
<p>Consistent styling across all inputs</p>
</div>
</div>
</dees-panel>
</section>
</div>
</div>
`;

6
ts_web/pages/mainpage.ts Normal file
View File

@ -0,0 +1,6 @@
import { html } from '@design.estate/dees-element';
export const mainPage = () => html`
<dees-input-text label="my-test-label"></dees-input-text>
<dees-input-text label="my-test-label"></dees-input-text>
`;

View File

@ -0,0 +1,759 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import { DeesModal } from '../elements/dees-modal.js';
import { DeesToast } from '../elements/dees-toast.js';
import { DeesContextmenu } from '../elements/dees-contextmenu.js';
import '../elements/dees-button.js';
import '../elements/dees-input-dropdown.js';
import '../elements/dees-form.js';
import '../elements/dees-panel.js';
import '../elements/dees-input-text.js';
import '../elements/dees-input-radiogroup.js';
import '../elements/dees-input-tags.js';
import '../elements/dees-appui-profiledropdown.js';
export const showcasePage = () => html`
<style>
${css`
.page-wrapper {
display: block;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
min-height: 100%;
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
}
.showcase-container {
max-width: 1200px;
margin: 0 auto;
padding: 48px 24px;
}
.showcase-header {
text-align: center;
margin-bottom: 48px;
}
.showcase-title {
font-size: 48px;
font-weight: 700;
margin: 0 0 16px 0;
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.showcase-subtitle {
font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')};
margin: 0 0 32px 0;
line-height: 1.6;
}
.showcase-section {
margin-bottom: 48px;
}
.showcase-section:last-child {
margin-bottom: 0;
padding-bottom: 48px;
}
/* Ensure all headings are theme-aware */
h1, h2, h3, h4, h5, h6 {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
margin: 0;
}
p {
color: ${cssManager.bdTheme('#666', '#999')};
line-height: 1.6;
}
strong {
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 32px;
}
.section-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
}
.section-icon.layers { background: ${cssManager.bdTheme('#f3e5f5', '#4a148c')}; }
.section-icon.registry { background: ${cssManager.bdTheme('#e8f5e9', '#1b5e20')}; }
.section-icon.demo { background: ${cssManager.bdTheme('#fff3e0', '#e65100')}; }
.section-icon.guidelines { background: ${cssManager.bdTheme('#e0f2f1', '#004d40')}; }
.section-title {
font-size: 32px;
font-weight: 600;
margin: 0;
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.section-description {
color: ${cssManager.bdTheme('#666', '#999')};
font-size: 16px;
line-height: 1.6;
margin-bottom: 24px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
.demo-card {
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 12px;
padding: 24px;
transition: all 0.2s ease;
}
.demo-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
}
.demo-card h4 {
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
}
.hierarchy-visual {
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 12px;
padding: 32px;
margin-bottom: 40px;
}
.hierarchy-visual h3 {
margin-top: 0;
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: ${cssManager.bdTheme('#1a1a1a', '#fff')};
}
.layer-stack {
display: flex;
flex-direction: column-reverse;
gap: 8px;
margin-top: 16px;
}
.layer {
padding: 16px 20px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'Geist Mono', monospace;
font-size: 14px;
transition: all 0.2s;
border: 1px solid transparent;
}
.layer:hover {
transform: translateX(4px);
border-color: ${cssManager.bdTheme('#e0e0e0', '#444')};
}
.layer.base {
background: ${cssManager.bdTheme('#f0f0f0', '#222')};
color: ${cssManager.bdTheme('#666', '#999')};
}
.layer.fixed {
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
color: ${cssManager.bdTheme('#1976d2', '#90caf9')};
}
.layer.dropdown {
background: ${cssManager.bdTheme('#f3e5f5', '#4a148c')};
color: ${cssManager.bdTheme('#7b1fa2', '#ce93d8')};
}
.layer.modal {
background: ${cssManager.bdTheme('#e8f5e9', '#1b5e20')};
color: ${cssManager.bdTheme('#388e3c', '#81c784')};
}
.layer.context {
background: ${cssManager.bdTheme('#fff3e0', '#e65100')};
color: ${cssManager.bdTheme('#f57c00', '#ffb74d')};
}
.layer.toast {
background: ${cssManager.bdTheme('#ffebee', '#b71c1c')};
color: ${cssManager.bdTheme('#d32f2f', '#ef5350')};
}
.layer-name {
font-weight: 600;
}
.layer-value {
opacity: 0.8;
}
.warning-box {
background: ${cssManager.bdTheme('#fff8e1', '#332701')};
border: 1px solid ${cssManager.bdTheme('#ffe082', '#664400')};
border-radius: 12px;
padding: 20px;
margin-bottom: 32px;
color: ${cssManager.bdTheme('#f57f17', '#ffecb5')};
display: flex;
align-items: flex-start;
gap: 12px;
}
.warning-box::before {
content: '⚠️';
font-size: 20px;
flex-shrink: 0;
}
.warning-box strong {
color: ${cssManager.bdTheme('#f57f17', '#ffd93d')};
}
code {
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
padding: 2px 6px;
border-radius: 3px;
font-family: 'Geist Mono', monospace;
font-size: 14px;
}
.test-area {
position: relative;
height: 200px;
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
border: 2px dashed ${cssManager.bdTheme('#ccc', '#444')};
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
}
.profile-demo {
position: relative;
display: inline-block;
}
.registry-status {
background: ${cssManager.bdTheme('#e8f5e9', '#1a2e1a')};
border: 1px solid ${cssManager.bdTheme('#4caf50', '#2e7d32')};
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.registry-status::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${cssManager.bdTheme('#4caf50', '#2e7d32')};
}
.registry-status h4 {
margin-top: 0;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#2e7d32', '#81c784')};
font-size: 18px;
font-weight: 600;
}
.registry-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
color: ${cssManager.bdTheme('#558b2f', '#aed581')};
font-family: 'Geist Mono', monospace;
font-size: 14px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0f2e1', '#1b5e20')};
}
.registry-item:last-child {
border-bottom: none;
}
.registry-item.active {
color: ${cssManager.bdTheme('#2e7d32', '#4ade80')};
font-weight: 600;
}
.registry-item span:last-child {
font-weight: 600;
}
`}
</style>
<div class="page-wrapper">
<div class="showcase-container">
<div class="showcase-header">
<h1 class="showcase-title">Z-Index Management</h1>
<p class="showcase-subtitle">
A comprehensive system for managing overlay stacking order across all components.
Test different scenarios to see how the dynamic z-index registry ensures proper layering.
</p>
</div>
<div class="warning-box">
<div>
<strong>Important:</strong> The z-index values are managed centrally in <code>00zindex.ts</code>.
Never use arbitrary z-index values in components - always import and use the z-index registry.
</div>
</div>
<!-- Registry Status Section -->
<div class="showcase-section">
<div class="section-header">
<div class="section-icon registry">📊</div>
<div>
<h2 class="section-title">Live Registry Status</h2>
</div>
</div>
<div class="registry-status" id="registryStatus">
<h4>Z-Index Registry</h4>
<div class="registry-item">
<span>Active Elements:</span>
<span id="activeCount">0</span>
</div>
<div class="registry-item">
<span>Current Z-Index:</span>
<span id="currentZIndex">1000</span>
</div>
</div>
</div>
<script>
// Update registry status periodically
setInterval(() => {
const registryDiv = document.getElementById('registryStatus');
if (registryDiv && window.zIndexRegistry) {
const activeCount = document.getElementById('activeCount');
const currentZIndex = document.getElementById('currentZIndex');
if (activeCount) activeCount.textContent = window.zIndexRegistry.getActiveCount();
if (currentZIndex) currentZIndex.textContent = window.zIndexRegistry.getCurrentZIndex();
// Update active state
const items = registryDiv.querySelectorAll('.registry-item');
const count = window.zIndexRegistry.getActiveCount();
if (count > 0) {
items[0].classList.add('active');
} else {
items[0].classList.remove('active');
}
}
}, 500);
// Make registry available globally for the demo
import('../elements/00zindex.js').then(module => {
window.zIndexRegistry = module.zIndexRegistry;
});
</script>
<!-- Layer Hierarchy Section -->
<div class="showcase-section">
<div class="section-header">
<div class="section-icon layers">📚</div>
<div>
<h2 class="section-title">Layer Hierarchy</h2>
</div>
</div>
<p class="section-description">
The traditional z-index layers are still defined for reference, but the new registry system
dynamically assigns z-indexes based on creation order.
</p>
<div class="hierarchy-visual">
<h3>Legacy Z-Index Layers (Reference)</h3>
<div class="layer-stack">
<div class="layer base">
<span class="layer-name">Base Content</span>
<span class="layer-value">z-index: auto</span>
</div>
<div class="layer fixed">
<span class="layer-name">Fixed Navigation</span>
<span class="layer-value">z-index: 10-250</span>
</div>
<div class="layer dropdown">
<span class="layer-name">Dropdown Overlays</span>
<span class="layer-value">z-index: 1999-2000</span>
</div>
<div class="layer modal">
<span class="layer-name">Modal Dialogs</span>
<span class="layer-value">z-index: 2999-3000</span>
</div>
<div class="layer context">
<span class="layer-name">Context Menus & WYSIWYG</span>
<span class="layer-value">z-index: 4000-4500</span>
</div>
<div class="layer toast">
<span class="layer-name">Toast Notifications</span>
<span class="layer-value">z-index: 5000</span>
</div>
</div>
</div>
</div>
<!-- Interactive Demos Section -->
<div class="showcase-section">
<div class="section-header">
<div class="section-icon demo">🎮</div>
<div>
<h2 class="section-title">Interactive Demos</h2>
</div>
</div>
<p class="section-description">
Test the z-index registry in action with these interactive examples. Each element gets the next
available z-index when created, ensuring proper stacking order.
</p>
<dees-panel .title=${'Basic Overlay Tests'} .subtitle=${'Test individual overlay components'}>
<div class="demo-grid">
<div class="demo-card">
<h4>Dropdown Test</h4>
<dees-input-dropdown
.label=${'Select Option'}
.options=${[
{option: 'Show Toast', key: 'toast', payload: 'toast'},
{option: 'Option 2', key: 'opt2', payload: '2'},
{option: 'Option 3', key: 'opt3', payload: '3'},
{option: 'Option 4', key: 'opt4', payload: '4'},
]}
@change=${async (e: CustomEvent) => {
if (e.detail.value?.payload === 'toast') {
DeesToast.createAndShow({ message: 'Toast appears above dropdown!', type: 'success' });
}
}}
></dees-input-dropdown>
</div>
<div class="demo-card">
<h4>Context Menu Test</h4>
<div class="test-area" @contextmenu=${(e: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(e, [
{ name: 'Show Toast', iconName: 'bell', action: async () => {
DeesToast.createAndShow({ message: 'Toast from context menu!', type: 'info' });
}},
{ divider: true },
{ name: 'Item 2', iconName: 'check', action: async () => {} },
{ name: 'Item 3', iconName: 'copy', action: async () => {} },
]);
}}>
<span style="color: ${cssManager.bdTheme('#999', '#666')}">Right-click here for context menu</span>
</div>
</div>
<div class="demo-card">
<h4>Toast Notification</h4>
<dees-button @click=${async () => {
DeesToast.createAndShow({ message: 'I appear on top of everything!', type: 'success' });
}}>Show Toast</dees-button>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Modal with Dropdown'} .subtitle=${'Critical test: Dropdown inside modal should appear above modal'}>
<p>This tests the most common z-index conflict scenario.</p>
<dees-button @click=${async () => {
const modal = await DeesModal.createAndShow({
heading: 'Modal with Dropdown',
width: 'medium',
showHelpButton: true,
onHelp: async () => {
DeesToast.createAndShow({ message: 'Help requested! Toast appears above modal.', type: 'info' });
},
content: html`
<p>The dropdown below should appear <strong>above</strong> this modal:</p>
<dees-form>
<dees-input-dropdown
.label=${'Select Country'}
.options=${[
{option: 'United States', key: 'us', payload: 'US'},
{option: 'Canada', key: 'ca', payload: 'CA'},
{option: 'United Kingdom', key: 'uk', payload: 'UK'},
{option: 'Germany', key: 'de', payload: 'DE'},
{option: 'France', key: 'fr', payload: 'FR'},
{option: 'Japan', key: 'jp', payload: 'JP'},
{option: 'Australia', key: 'au', payload: 'AU'},
{option: 'Brazil', key: 'br', payload: 'BR'},
]}
.required=${true}
></dees-input-dropdown>
<dees-input-text
.label=${'Additional Field'}
.placeholder=${'Just to show form context'}
></dees-input-text>
<dees-input-tags
.label=${'Tags'}
.placeholder=${'Add tags...'}
.suggestions=${['urgent', 'bug', 'feature', 'documentation', 'testing']}
.description=${'Add relevant tags'}
></dees-input-tags>
</dees-form>
<p style="margin-top: 16px; color: ${cssManager.bdTheme('#666', '#999')}">
You can also right-click anywhere in this modal to test context menus.
</p>
`,
menuOptions: [
{ name: 'Cancel', action: async (modal) => modal.destroy() },
{ name: 'Save', action: async (modal) => modal.destroy() }
]
});
// Add context menu to modal content
const modalContent = modal.shadowRoot.querySelector('.modal .content');
if (modalContent) {
modalContent.addEventListener('contextmenu', async (e: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(e, [
{ name: 'Context menu in modal', iconName: 'check', action: async () => {} },
{ divider: true },
{ name: 'Show Toast', iconName: 'bell', action: async () => {
DeesToast.createAndShow({ message: 'Toast from modal context menu!', type: 'warning' });
}}
]);
});
}
}}>Open Modal with Dropdown</dees-button>
</dees-panel>
<dees-panel .title=${'Complex Stacking Scenario'} .subtitle=${'Multiple overlays active simultaneously'}>
<p>This creates a complex scenario with multiple overlays to test the complete hierarchy.</p>
<dees-button @click=${async () => {
// Show base modal
await DeesModal.createAndShow({
heading: 'Base Modal',
width: 'large',
content: html`
<h4>Level 1: Modal</h4>
<p>This is the base modal. Try the following:</p>
<ol>
<li>Open the dropdown below</li>
<li>Right-click for context menu</li>
<li>Click "Show Second Modal" to stack modals</li>
</ol>
<dees-input-dropdown
.label=${'Test Dropdown in Modal'}
.options=${[
{option: 'Trigger Toast', key: 'toast', payload: 'toast'},
{option: 'Option 2', key: 'opt2', payload: '2'},
{option: 'Option 3', key: 'opt3', payload: '3'},
]}
@change=${async (e: CustomEvent) => {
if (e.detail.value?.payload === 'toast') {
DeesToast.createAndShow({ message: 'Toast triggered from dropdown in modal!', type: 'success' });
}
}}
></dees-input-dropdown>
<div style="margin-top: 16px;">
<dees-button @click=${async () => {
await DeesModal.createAndShow({
heading: 'Second Modal',
width: 'small',
content: html`
<h4>Level 2: Stacked Modal</h4>
<p>This modal appears on top of the first one.</p>
<p>The dropdown here should still work:</p>
<dees-input-dropdown
.label=${'Nested Dropdown'}
.options=${[
{option: 'Option A', key: 'a'},
{option: 'Option B', key: 'b'},
{option: 'Option C', key: 'c'},
]}
></dees-input-dropdown>
`,
menuOptions: [
{ name: 'Close', action: async (modal) => modal.destroy() }
]
});
}}>Show Second Modal</dees-button>
</div>
`,
menuOptions: [
{ name: 'Close All', action: async (modal) => {
modal.destroy();
// Also show a toast
DeesToast.createAndShow({ message: 'All modals closed!', type: 'info' });
}}
]
});
}}>Start Complex Stack Test</dees-button>
</dees-panel>
<dees-panel .title=${'Profile Dropdown'} .subtitle=${'Testing app UI dropdowns'}>
<p>Profile dropdowns and similar UI elements use the dropdown z-index layer.</p>
<div class="profile-demo">
<dees-appui-profiledropdown
.user=${{
name: 'Test User',
email: 'test@example.com',
avatar: 'https://randomuser.me/api/portraits/lego/1.jpg',
status: 'online' as const
}}
.menuItems=${[
{ name: 'Show Toast', iconName: 'bell', shortcut: '', action: async () => {
DeesToast.createAndShow({ message: 'Profile action triggered!', type: 'success' });
}},
{ divider: true } as const,
{ name: 'Settings', iconName: 'settings', shortcut: '', action: async () => {} },
{ name: 'Logout', iconName: 'logOut', shortcut: '', action: async () => {} }
]}
></dees-appui-profiledropdown>
</div>
</dees-panel>
<dees-panel .title=${'Edge Cases'} .subtitle=${'Special scenarios and gotchas'}>
<div class="demo-grid">
<div class="demo-card">
<h4>Multiple Toasts</h4>
<dees-button @click=${async () => {
DeesToast.createAndShow({ message: 'First toast', type: 'info' });
setTimeout(() => {
DeesToast.createAndShow({ message: 'Second toast', type: 'warning' });
}, 500);
setTimeout(() => {
DeesToast.createAndShow({ message: 'Third toast', type: 'success' });
}, 1000);
}}>Show Multiple Toasts</dees-button>
</div>
<div class="demo-card">
<h4>Modal with Tags Input</h4>
<dees-button @click=${async () => {
await DeesModal.createAndShow({
heading: 'Tags Input Test',
width: 'medium',
content: html`
<p>Test the tags input component in a modal:</p>
<dees-form>
<dees-input-tags
.label=${'Search Terms'}
.placeholder=${'Enter search terms...'}
.value=${['typescript', 'modal']}
.suggestions=${[
'javascript', 'typescript', 'css', 'html',
'react', 'vue', 'angular', 'svelte',
'modal', 'dropdown', 'form', 'input'
]}
.description=${'Add search terms to filter results'}
></dees-input-tags>
<dees-input-tags
.label=${'Categories'}
.placeholder=${'Add categories...'}
.required=${true}
.maxTags=${3}
.description=${'Select up to 3 categories'}
></dees-input-tags>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modal) => modal.destroy() },
{ name: 'Apply', action: async (modal) => {
DeesToast.createAndShow({ message: 'Tags applied!', type: 'success' });
modal.destroy();
}}
]
});
}}>Test Tags in Modal</dees-button>
</div>
<div class="demo-card">
<h4>Fullscreen Modal</h4>
<dees-button @click=${async () => {
await DeesModal.createAndShow({
heading: 'Fullscreen Modal Test',
width: 'fullscreen',
content: html`
<p>Even in fullscreen, overlays should work properly:</p>
<dees-input-radiogroup
.label=${'Select Option'}
.options=${['Option 1', 'Option 2', 'Option 3']}
></dees-input-radiogroup>
<dees-input-dropdown
.label=${'Dropdown in Fullscreen'}
.options=${[
{option: 'Works properly', key: '1'},
{option: 'Above modal', key: '2'},
]}
></dees-input-dropdown>
`,
menuOptions: [
{ name: 'Exit Fullscreen', action: async (modal) => modal.destroy() }
]
});
}}>Open Fullscreen</dees-button>
</div>
</div>
</dees-panel>
</div>
<!-- Guidelines Section -->
<div class="showcase-section">
<div class="section-header">
<div class="section-icon guidelines">📖</div>
<div>
<h2 class="section-title">Usage Guidelines</h2>
</div>
</div>
<dees-panel>
<h4>Best Practices:</h4>
<ul>
<li>Always use the z-index registry from <code>00zindex.ts</code></li>
<li>Never use arbitrary z-index values like <code>z-index: 9999</code></li>
<li>Get z-index from registry when showing elements: <code>zIndexRegistry.getNextZIndex()</code></li>
<li>Register elements to track them: <code>zIndexRegistry.register(element, zIndex)</code></li>
<li>Unregister on cleanup: <code>zIndexRegistry.unregister(element)</code></li>
<li>Elements created later automatically appear on top</li>
<li>Test overlay interactions, especially dropdowns in modals</li>
</ul>
<h4>Import Example:</h4>
<pre style="background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')}; padding: 16px; border-radius: 6px; overflow-x: auto;">
<code>import { zIndexRegistry } from './00zindex.js';
// In your component:
const myZIndex = zIndexRegistry.getNextZIndex();
element.style.zIndex = myZIndex.toString();
zIndexRegistry.register(element, myZIndex);
// On cleanup:
zIndexRegistry.unregister(element);</code></pre>
</dees-panel>
</div>
</div>
</div>
`;