feat: migrate dashboard to svelte 5 (#577)
* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: checkpoint * feat: update editor * [not working] connect everything with ui build * storybook + ui + dashboard all working independently * add bg-black to +page.svelte * feat: add editor to storybook * chore: animate lucid icons (#590) * v1 animations * feat: change some course navigation icons & animate org navigation icons * chore: change settings icon * chore: use css instead of js for hover * rename animations --------- Co-authored-by: tunny17 <[email protected]> * chore: update path to node_modules types in api tsconfig * fix: update import path for DropdownMenu component * fix: add rollupOptions to build configuration in vite.config.ts * fix: remove rollupOptions from build configuration and add @floating-ui/dom dependency * install --------- Co-authored-by: tunny17 <[email protected]>
This commit is contained in:
+6
-1
@@ -37,4 +37,9 @@ yarn-error.log*
|
|||||||
|
|
||||||
vite.config.ts*.mjs
|
vite.config.ts*.mjs
|
||||||
|
|
||||||
.pnpm-store/*
|
.pnpm-store/*
|
||||||
|
|
||||||
|
**/dist/*
|
||||||
|
|
||||||
|
#Ignore vscode AI rules
|
||||||
|
.github/instructions/codacy.instructions.md
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.19.0",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@typescript-eslint/eslint-plugin": "7.0.1",
|
"@typescript-eslint/eslint-plugin": "7.0.1",
|
||||||
"@typescript-eslint/parser": "7.0.1",
|
"@typescript-eslint/parser": "7.0.1",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"typeRoots": ["./node_modules/@types", "./src/types"],
|
"typeRoots": ["../../node_modules/@types", "./node_modules/@types", "./src/types"],
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
|
|
||||||
};
|
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@carbon/charts-svelte": "^1.13.6",
|
"@carbon/charts-svelte": "^1.13.6",
|
||||||
"@cio/api": "workspace:*",
|
"@cio/api": "workspace:*",
|
||||||
|
"@cio/ui": "workspace:*",
|
||||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
"@lucide/svelte": "^0.545.0",
|
"@lucide/svelte": "^0.545.0",
|
||||||
"@polar-sh/sveltekit": "^0.2.1",
|
"@polar-sh/sveltekit": "^0.2.1",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"@types/pluralize": "0.0.30",
|
"@types/pluralize": "0.0.30",
|
||||||
"ai": "^2.1.31",
|
"ai": "^2.1.31",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
|
"bits-ui": "^2.14.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"bufferutil": "^4.0.9",
|
"bufferutil": "^4.0.9",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
@@ -78,8 +80,10 @@
|
|||||||
"@babel/runtime": "^7.22.6",
|
"@babel/runtime": "^7.22.6",
|
||||||
"@eslint/compat": "^1.4.0",
|
"@eslint/compat": "^1.4.0",
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@rollup/plugin-replace": "^6.0.2",
|
||||||
"@sveltejs/kit": "^2.5.27",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"@tailwindcss/postcss": "^4.1.14",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@testing-library/svelte": "^4.0.5",
|
"@testing-library/svelte": "^4.0.5",
|
||||||
@@ -87,7 +91,6 @@
|
|||||||
"@types/lodash": "^4.14.196",
|
"@types/lodash": "^4.14.196",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"all-object-keys": "^2.2.0",
|
"all-object-keys": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"carbon-components-svelte": "^0.79.0",
|
"carbon-components-svelte": "^0.79.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
@@ -107,7 +110,7 @@
|
|||||||
"svelte-jester": "^3.0.0",
|
"svelte-jester": "^3.0.0",
|
||||||
"svelte-loading-spinners": "^0.3.4",
|
"svelte-loading-spinners": "^0.3.4",
|
||||||
"svelte-preprocess": "^6.0.0",
|
"svelte-preprocess": "^6.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^4.1.14",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"@cio/tsconfig": "workspace:*",
|
"@cio/tsconfig": "workspace:*",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/*Write your global styles here, in PostCSS syntax */
|
/*Write your global styles here, in PostCSS syntax */
|
||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
@import '@cio/ui/output.css';
|
||||||
@tailwind utilities;
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
/** Global scrollbar styles */
|
/** Global scrollbar styles */
|
||||||
html {
|
html {
|
||||||
@@ -84,7 +85,7 @@ h6 {
|
|||||||
margin-bottom: 10px !important;
|
margin-bottom: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .prose h1,
|
/* .dark .prose h1,
|
||||||
.dark .prose h2,
|
.dark .prose h2,
|
||||||
.dark .prose h3,
|
.dark .prose h3,
|
||||||
.dark .prose h4,
|
.dark .prose h4,
|
||||||
@@ -95,7 +96,7 @@ h6 {
|
|||||||
.dark .prose li::marker,
|
.dark .prose li::marker,
|
||||||
.dark .prose p {
|
.dark .prose p {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
} */
|
||||||
|
|
||||||
iframe.iframe {
|
iframe.iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -192,10 +193,6 @@ svg.lucide-icon:not(.custom) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@tailwind components;
|
|
||||||
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rose Theme
|
Rose Theme
|
||||||
*/
|
*/
|
||||||
@@ -710,3 +707,312 @@ svg.lucide-icon:not(.custom) {
|
|||||||
.theme-violet .border-primary-900 {
|
.theme-violet .border-primary-900 {
|
||||||
border-color: #890684 !important;
|
border-color: #890684 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tailwind v4 Custom Animations and Keyframes */
|
||||||
|
@theme {
|
||||||
|
--animate-shake-announcement: shakeAnnouncement 0.5s ease-in-out;
|
||||||
|
--animate-flip-book: flipBook 0.6s ease-in-out;
|
||||||
|
--animate-scroll-unroll: scrollUnroll 0.8s ease-out;
|
||||||
|
--animate-check-items: attendanceCount 0.8s ease-in-out;
|
||||||
|
--animate-task-complete: taskComplete 0.6s ease-in-out;
|
||||||
|
--animate-grade-glow: gradeGlow 1s ease-in-out;
|
||||||
|
--animate-people-wave: peopleWave 0.8s ease-in-out;
|
||||||
|
--animate-chart-grow: dataScan 1s ease-in-out;
|
||||||
|
--animate-screen-glow: screenGlow 1s ease-in-out;
|
||||||
|
--animate-library-expand: libraryExpand 0.7s ease-out;
|
||||||
|
--animate-template-morph: templateMorph 0.6s ease-in-out;
|
||||||
|
--animate-ripple: communityRipple 0.9s ease-out;
|
||||||
|
--animate-roll: quizRoll 0.8s ease-in-out;
|
||||||
|
--animate-gather: audienceGather 0.7s ease-in-out;
|
||||||
|
--animate-setup-configure: setupConfigure 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shakeAnnouncement {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70%,
|
||||||
|
90% {
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
40%,
|
||||||
|
60%,
|
||||||
|
80% {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flipBook {
|
||||||
|
0% {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotateY(90deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scrollUnroll {
|
||||||
|
0% {
|
||||||
|
transform: scaleY(0) translateY(-50%);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scaleY(0.8) translateY(-25%);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleY(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes attendanceCount {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes taskComplete {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: scale(1.1) rotate(-5deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2) rotate(5deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: scale(1.1) rotate(-2deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradeGlow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: brightness(1) drop-shadow(0 0 0 rgba(34, 197, 94, 0));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
filter: brightness(1.3) drop-shadow(0 0 8px rgba(34, 197, 94, 0.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes peopleWave {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(-5deg) scale(1.05);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(5deg) scale(1.1);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-3deg) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dataScan {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0));
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
filter: brightness(1.1) drop-shadow(0 0 4px rgba(99, 102, 241, 0.4));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
filter: brightness(1.2) drop-shadow(0 0 8px rgba(99, 102, 241, 0.6));
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: scale(1.15) rotate(2deg);
|
||||||
|
filter: brightness(1.3) drop-shadow(0 0 12px rgba(99, 102, 241, 0.8));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: brightness(1) drop-shadow(0 0 0 rgba(99, 102, 241, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes screenGlow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: brightness(1) drop-shadow(0 0 0 rgba(59, 130, 246, 0));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
filter: brightness(1.2) drop-shadow(0 0 6px rgba(59, 130, 246, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes libraryExpand {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100%) scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-20%) scale(0.9);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(5%) scale(1.05);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateY(-2%) scale(1.02);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes templateMorph {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: scale(1.05) rotate(-2deg) skewX(2deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1) rotate(2deg) skewX(-2deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: scale(1.05) rotate(-1deg) skewX(1deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) rotate(0deg) skewX(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes communityRipple {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%) scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateX(-20%) scale(0.9);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateX(5%) scale(1.05);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateX(-2%) scale(1.02);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes quizRoll {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: scale(1.05) rotate(72deg);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1.1) rotate(144deg);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(1.15) rotate(216deg);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scale(1.1) rotate(288deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes audienceGather {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0));
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: scale(0.95) scaleX(1.1);
|
||||||
|
filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1) scaleX(0.9);
|
||||||
|
filter: brightness(1.1) drop-shadow(0 0 4px rgba(147, 51, 234, 0.5));
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: scale(1.05) scaleX(1.05);
|
||||||
|
filter: brightness(1.05) drop-shadow(0 0 2px rgba(147, 51, 234, 0.3));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) scaleX(1);
|
||||||
|
filter: brightness(1) drop-shadow(0 0 0 rgba(147, 51, 234, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes setupConfigure {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
15% {
|
||||||
|
transform: scale(1.02) rotate(15deg);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: scale(1.04) rotate(-10deg);
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
transform: scale(1.06) rotate(20deg);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(1.08) rotate(-15deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: scale(1.06) rotate(10deg);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: scale(1.02) rotate(-5deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-25
@@ -1,33 +1,31 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
||||||
<meta name="theme-color" content="#333333" />
|
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<head>
|
||||||
<link rel="icon" type="image/png" href="/favicon.ico" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
<meta name="theme-color" content="#333333" />
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.ico" />
|
<link rel="icon" type="image/png" href="/favicon.ico" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
|
||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
||||||
<link rel="stylesheet" href="https://assets.cdn.clsrio.com/carbon.css" />
|
|
||||||
|
|
||||||
<script
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
defer
|
<link rel="icon" type="image/png" href="/favicon.ico" />
|
||||||
src="https://umami.hz.oncws.com/script.js"
|
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
|
||||||
data-website-id="80a9544a-4dda-4c91-b62f-b6be7a8a3b5c"
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
||||||
></script>
|
|
||||||
|
|
||||||
<script src="https://cdn.plyr.io/3.7.3/plyr.js"></script>
|
<script defer src="https://umami.hz.oncws.com/script.js"
|
||||||
|
data-website-id="80a9544a-4dda-4c91-b62f-b6be7a8a3b5c"></script>
|
||||||
|
|
||||||
%sveltekit.head%
|
<script src="https://cdn.plyr.io/3.7.3/plyr.js"></script>
|
||||||
</head>
|
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover" class="">
|
%sveltekit.head%
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
</head>
|
||||||
</body>
|
|
||||||
</html>
|
<body data-sveltekit-preload-data="hover" class="">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
import TabContent from '$lib/components/TabContent/index.svelte';
|
import TabContent from '$lib/components/TabContent/index.svelte';
|
||||||
import Tabs from '$lib/components/Tabs/index.svelte';
|
import Tabs from '$lib/components/Tabs/index.svelte';
|
||||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
// import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||||
import MODES from '$lib/utils/constants/mode';
|
import MODES from '$lib/utils/constants/mode';
|
||||||
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
@@ -67,16 +67,27 @@
|
|||||||
toggleMode = () => {}
|
toggleMode = () => {}
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
// let content = $state<EdraEditorProps['Content']>();
|
||||||
|
// let editor = $state<EdraEditorProps['Editor']>();
|
||||||
|
// function onUpdate() {
|
||||||
|
// content = editor?.getHTML();
|
||||||
|
// console.log('content updated:', content);
|
||||||
|
// if (mode === MODES.view) return;
|
||||||
|
|
||||||
|
// $lessonByTranslation[lessonId][$lesson.locale] = content;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// localStorage.setItem(`lesson-${lessonId}-${$lesson.locale}`, content);
|
||||||
|
// } catch (error) {}
|
||||||
|
// $isLessonDirty = true;
|
||||||
|
// }
|
||||||
|
|
||||||
let localeExists: Record<string, boolean> = {};
|
let localeExists: Record<string, boolean> = {};
|
||||||
// let prevContent = '';
|
// let prevContent = '';
|
||||||
let timeoutId: NodeJS.Timeout;
|
let timeoutId: NodeJS.Timeout;
|
||||||
let errors: Record<string, string> = {};
|
let errors: Record<string, string> = {};
|
||||||
let editorWindowRef: Window | undefined = $state();
|
// let editorWindowRef: Window | undefined = $state();
|
||||||
let aiButtonRef: HTMLDivElement | undefined = $state();
|
('flex items-center gap-2 px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2');
|
||||||
let openPopover = $state(false);
|
|
||||||
let player: HTMLVideoElement | undefined = $state();
|
|
||||||
let aiButtonClass =
|
|
||||||
'flex items-center gap-2 px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2';
|
|
||||||
|
|
||||||
const lessonTitle = $derived($lesson.title);
|
const lessonTitle = $derived($lesson.title);
|
||||||
|
|
||||||
@@ -324,7 +335,7 @@
|
|||||||
initPlyr(player, $lesson.materials.videos);
|
initPlyr(player, $lesson.materials.videos);
|
||||||
});
|
});
|
||||||
|
|
||||||
let editorValue = $derived(
|
let _editorValue = $derived(
|
||||||
lessonFallbackNote($lesson.materials.note, $lessonByTranslation[lessonId], $lesson.locale)
|
lessonFallbackNote($lesson.materials.note, $lessonByTranslation[lessonId], $lesson.locale)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -403,7 +414,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 h-[60vh]">
|
<div class="mt-5 h-[60vh]">
|
||||||
<TextEditor
|
<!-- <div class="bg-background z-50 mt-12 size-full max-w-5xl rounded-md border border-dashed">
|
||||||
|
{#if editor && !editor.isDestroyed}
|
||||||
|
<EdraToolBar
|
||||||
|
class="bg-secondary/50 flex w-full items-center overflow-x-auto border-b border-dashed p-0.5"
|
||||||
|
{editor}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
Debug: Editor state - {editor ? 'exists' : 'undefined'}, destroyed: {editor?.isDestroyed || 'N/A'}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<EdraEditor
|
||||||
|
bind:editor
|
||||||
|
content={editorValue}
|
||||||
|
autofocus={true}
|
||||||
|
class="h-[300px] outline-none"
|
||||||
|
{onUpdate}
|
||||||
|
/>
|
||||||
|
</div> -->
|
||||||
|
<!-- <TextEditor
|
||||||
id={lessonId}
|
id={lessonId}
|
||||||
bind:editorWindowRef
|
bind:editorWindowRef
|
||||||
value={editorValue}
|
value={editorValue}
|
||||||
@@ -419,7 +449,7 @@
|
|||||||
$isLessonDirty = true;
|
$isLessonDirty = true;
|
||||||
}}
|
}}
|
||||||
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
|
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
|
||||||
/>
|
/> -->
|
||||||
</div>
|
</div>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
<div class={className}>
|
<div class={className}>
|
||||||
<button
|
<button
|
||||||
class="item relative flex items-center {btnPadding} mb-1 ml-2 {NavClasses.item} {isGroupActive &&
|
class="item group relative flex items-center {btnPadding} mb-1 ml-2 {NavClasses.item} {isGroupActive &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
NavClasses.active} w-[95%]"
|
NavClasses.active} w-[95%]"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents'; // overview
|
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents';
|
||||||
import BookCheckIcon from '@lucide/svelte/icons/book-check';
|
import BookCheckIcon from '@lucide/svelte/icons/book-check';
|
||||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
|
||||||
import UsersIcon from '@lucide/svelte/icons/users';
|
import UsersIcon from '@lucide/svelte/icons/users';
|
||||||
import ScreenShareIcon from '@lucide/svelte/icons/screen-share';
|
|
||||||
import AwardIcon from '@lucide/svelte/icons/award';
|
import AwardIcon from '@lucide/svelte/icons/award';
|
||||||
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text';
|
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text';
|
||||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||||
import MegaphoneIcon from '@lucide/svelte/icons/megaphone';
|
import MegaphoneIcon from '@lucide/svelte/icons/megaphone';
|
||||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||||
import ListIcon from '@lucide/svelte/icons/list';
|
import SquareLibrary from '@lucide/svelte/icons/square-library';
|
||||||
|
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
|
||||||
|
import PanelTop from '@lucide/svelte/icons/panel-top';
|
||||||
import { NAV_IDS } from './constants';
|
import { NAV_IDS } from './constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,25 +20,25 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if name === NAV_IDS.SECTION}
|
{#if name === NAV_IDS.SECTION}
|
||||||
<TableOfContentsIcon size={16} />
|
<TableOfContentsIcon size={16} class="group-hover:animate-flip-book" />
|
||||||
{:else if name === NAV_IDS.NEWS_FEED}
|
{:else if name === NAV_IDS.NEWS_FEED}
|
||||||
<MegaphoneIcon size={16} />
|
<MegaphoneIcon size={16} class="group-hover:animate-shake-announcement" />
|
||||||
{:else if name === NAV_IDS.LESSONS}
|
{:else if name === NAV_IDS.LESSONS}
|
||||||
<ScrollTextIcon size={16} />
|
<ScrollTextIcon size={16} class="group-hover:animate-scroll-unroll" />
|
||||||
{:else if name === NAV_IDS.ATTENDANCE}
|
{:else if name === NAV_IDS.ATTENDANCE}
|
||||||
<ListIcon size={16} />
|
<BookOpenCheck size={16} class="group-hover:animate-check-items" />
|
||||||
{:else if name === NAV_IDS.SUBMISSIONS}
|
{:else if name === NAV_IDS.SUBMISSIONS}
|
||||||
<ListTodoIcon size={16} />
|
<SquareLibrary size={16} class="group-hover:animate-task-complete" />
|
||||||
{:else if name === NAV_IDS.MARKS}
|
{:else if name === NAV_IDS.MARKS}
|
||||||
<BookCheckIcon size={16} />
|
<BookCheckIcon size={16} class="group-hover:animate-grade-glow" />
|
||||||
{:else if name === NAV_IDS.PEOPLE}
|
{:else if name === NAV_IDS.PEOPLE}
|
||||||
<UsersIcon size={16} />
|
<UsersIcon size={16} class="group-hover:animate-people-wave" />
|
||||||
{:else if name === NAV_IDS.ANALYTICS}
|
{:else if name === NAV_IDS.ANALYTICS}
|
||||||
<ChartLineIcon size={16} />
|
<ChartLineIcon size={16} class="group-hover:animate-chart-grow" />
|
||||||
{:else if name === NAV_IDS.LANDING_PAGE}
|
{:else if name === NAV_IDS.LANDING_PAGE}
|
||||||
<ScreenShareIcon size={16} />
|
<PanelTop size={16} class="group-hover:animate-screen-glow" />
|
||||||
{:else if name === NAV_IDS.CERTIFICATES}
|
{:else if name === NAV_IDS.CERTIFICATES}
|
||||||
<AwardIcon size={16} />
|
<AwardIcon size={16} class="group-hover:animate-bounce" />
|
||||||
{:else if name === NAV_IDS.SETTINGS}
|
{:else if name === NAV_IDS.SETTINGS}
|
||||||
<SettingsIcon size={16} />
|
<SettingsIcon size={16} class="group-hover:animate-spin" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = (await response.json());
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(result.error || $t('courses.copy_course.error.failed_to_clone'));
|
throw new Error(result.error || $t('courses.copy_course.error.failed_to_clone'));
|
||||||
|
|||||||
@@ -34,11 +34,6 @@
|
|||||||
emptyDescription = $t('courses.course_card.empty_description'),
|
emptyDescription = $t('courses.course_card.empty_description'),
|
||||||
isExplore = false
|
isExplore = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
export let courses: Course[] = [];
|
|
||||||
export let emptyTitle = $t('courses.course_card.empty_title');
|
|
||||||
export let emptyDescription = $t('courses.course_card.empty_description');
|
|
||||||
export let isExplore = false;
|
|
||||||
|
|
||||||
function calcProgressRate(progressRate?: number, totalLessons?: number): number {
|
function calcProgressRate(progressRate?: number, totalLessons?: number): number {
|
||||||
if (!progressRate || !totalLessons) {
|
if (!progressRate || !totalLessons) {
|
||||||
@@ -55,19 +50,19 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteCourse($deleteCourseModal.id);
|
await deleteCourse($deleteCourseModal.id);
|
||||||
|
|
||||||
// Remove the course from the courses store
|
// Remove the course from the courses store
|
||||||
$coursesStore = $coursesStore.filter(course => course.id !== $deleteCourseModal.id);
|
$coursesStore = $coursesStore.filter((course) => course.id !== $deleteCourseModal.id);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
snackbar.success('snackbar.course_deleted');
|
snackbar.success('snackbar.course_deleted');
|
||||||
|
|
||||||
// Close modal and reset state
|
// Close modal and reset state
|
||||||
deleteCourseModal.set(deleteCourseModalInitialState);
|
deleteCourseModal.set(deleteCourseModalInitialState);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting course:', error);
|
console.error('Error deleting course:', error);
|
||||||
snackbar.error('snackbar.course_settings.error.went_wrong');
|
snackbar.error('snackbar.course_settings.error.went_wrong');
|
||||||
|
|
||||||
// Stop deleting state on error
|
// Stop deleting state on error
|
||||||
$deleteCourseModal.isDeleting = false;
|
$deleteCourseModal.isDeleting = false;
|
||||||
}
|
}
|
||||||
@@ -75,8 +70,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CopyCourseModal />
|
<CopyCourseModal />
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
onDelete={handleDeleteCourse}
|
onDelete={handleDeleteCourse}
|
||||||
bind:open={$deleteCourseModal.open}
|
bind:open={$deleteCourseModal.open}
|
||||||
isLoading={$deleteCourseModal.isDeleting}
|
isLoading={$deleteCourseModal.isDeleting}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import defaultContent from './default-content';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor content={defaultContent} />
|
||||||
@@ -0,0 +1,825 @@
|
|||||||
|
export default {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: 'center',
|
||||||
|
level: 1
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Introducing ClassroomIO'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: 'justify',
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'The AI native LMS. We have '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'bold'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'bold'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'italic'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'italic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'strike'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'strike'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'underline'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'underline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: 'https://github.com',
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
class: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'link'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'code'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'code'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', Superscript A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'superscript'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'b'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', subscript A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'subscript'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'b'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'highlight',
|
||||||
|
attrs: {
|
||||||
|
color: '#800080'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'highlight1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'highlight',
|
||||||
|
attrs: {
|
||||||
|
color: '#1a1600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: ','
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'highlight',
|
||||||
|
attrs: {
|
||||||
|
color: '#0000FF'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'highlight2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'textStyle',
|
||||||
|
attrs: {
|
||||||
|
color: '#008000',
|
||||||
|
fontSize: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'textcolor1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'textStyle',
|
||||||
|
attrs: {
|
||||||
|
color: 'rgb(78, 211, 21)',
|
||||||
|
fontSize: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: ', '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'textStyle',
|
||||||
|
attrs: {
|
||||||
|
color: '#FFA500',
|
||||||
|
fontSize: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'textcolor2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'blockquote',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: 'justify',
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'This is a block Quote'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'codeBlock',
|
||||||
|
attrs: {
|
||||||
|
language: 'javascript'
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'console.log("Hello World")'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'horizontalRule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: 'left',
|
||||||
|
level: 2
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Support For Media Placeholders and Medias'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image-placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://placehold.co/800x400/6A00F5/white',
|
||||||
|
alt: null,
|
||||||
|
title: 'A Sample Image',
|
||||||
|
width: 658,
|
||||||
|
height: null,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'video-placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'video',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://videos.pexels.com/video-files/2491284/2491284-uhd_2732_1440_24fps.mp4',
|
||||||
|
alt: null,
|
||||||
|
title: 'A Sample Video',
|
||||||
|
width: 673,
|
||||||
|
height: null,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'audio-placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'audio',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://cdn.pixabay.com/audio/2024/10/30/audio_42e6870f29.mp3',
|
||||||
|
alt: null,
|
||||||
|
title: 'A Sample Audio',
|
||||||
|
width: 670,
|
||||||
|
height: null,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'horizontalRule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
level: 2
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'IFrames'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'iframe-placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'iframe',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3748.5645078258503!2d75.17451197505285!3d20.026784381383496!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3bdb93bd138ae4bd%3A0x574c6482cf0b89cf!2sEllora%20Caves!5e0!3m2!1sen!2sin!4v1742466514831!5m2!1sen!2sin',
|
||||||
|
alt: null,
|
||||||
|
title: 'Ellora Caves, Maharastra, India',
|
||||||
|
width: 621,
|
||||||
|
height: null,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'iframe',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://www.youtube.com/embed/jfKfPfyJRdk?si=SYBE0PkI5v79-54W',
|
||||||
|
alt: null,
|
||||||
|
title: 'Lofi Hip Hop Youtube Streaming Video',
|
||||||
|
width: 623,
|
||||||
|
height: null,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'horizontalRule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
level: 2
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Typography'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Smilipicker, type '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'code'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: ':)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' and it becomes 🙂 or 😉. Typography support e.g. type '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'code'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: '!='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' and it becomes ≠. Similarly (c) becomes ©, -> becomes → and many more like 1×2, ½. We also have color visualizer. #FFF, #000, #FF00FF can be visualized.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'horizontalRule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
level: 2
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Tables'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tableRow',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tableCell',
|
||||||
|
attrs: {
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
colwidth: [323],
|
||||||
|
style: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello World'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tableCell',
|
||||||
|
attrs: {
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
colwidth: [328],
|
||||||
|
style: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello World'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tableCell',
|
||||||
|
attrs: {
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
colwidth: [330],
|
||||||
|
style: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello World'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tableRow',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tableCell',
|
||||||
|
attrs: {
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
colwidth: [323],
|
||||||
|
style: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello World'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tableCell',
|
||||||
|
attrs: {
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
colwidth: [328],
|
||||||
|
style: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello World'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tableCell',
|
||||||
|
attrs: {
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
colwidth: [330],
|
||||||
|
style: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Hello World'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
level: 3
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'textStyle',
|
||||||
|
attrs: {
|
||||||
|
color: 'var(--tw-prose-headings)',
|
||||||
|
fontSize: '1.5em'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
text: 'Rending Math and '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex: '\\LaTeX',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'The editor supports the '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex: '\\LaTeX',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' rendering. e.g.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'orderedList',
|
||||||
|
attrs: {
|
||||||
|
start: 1
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'listItem',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Functions like, '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex: 'sin²\\theta + cos²\\theta=1, a²+b²=c²',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'listItem',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Matrix like unit matrix'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex: '\\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'or more matrices like'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex:
|
||||||
|
'\\begin{pmatrix} 1 & 0 & \\cdots & 0 \\\\ 0 & 1 & \\cdots & 0 \\\\ \\vdots & \\vdots & \\ddots & \\vdots \\\\ 0 & 0 & \\cdots & 1 \\end{pmatrix}',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'listItem',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Integrals and differential'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex:
|
||||||
|
'\\int_{0}^{\\frac{\\pi}{2}} \\sin(x) \\,dx = \\left[-\\cos(x)\\right]_0^{\\frac{\\pi}{2}} = -\\cos\\left(\\frac{\\pi}{2}\\right) + \\cos(0)',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'inlineMath',
|
||||||
|
attrs: {
|
||||||
|
latex: '\\frac{\\mathrm{d}}{\\mathrm{d}x}(\\sin x) = \\cos x',
|
||||||
|
evaluate: 'no',
|
||||||
|
display: 'no'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
level: 3
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Explore More…'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null,
|
||||||
|
class: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -11,11 +11,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { open = $bindable(false), onDelete = () => {}, isLoading = false }: Props = $props();
|
let { open = $bindable(false), onDelete = () => {}, isLoading = false }: Props = $props();
|
||||||
|
|
||||||
function handleDelete() {
|
|
||||||
onDelete();
|
|
||||||
open = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/environment';
|
|
||||||
import BellIcon from '@lucide/svelte/icons/bell';
|
import BellIcon from '@lucide/svelte/icons/bell';
|
||||||
import MoonIcon from '@lucide/svelte/icons/moon';
|
|
||||||
import SunIcon from '@lucide/svelte/icons/sun';
|
|
||||||
import MenuIcon from '@lucide/svelte/icons/menu';
|
import MenuIcon from '@lucide/svelte/icons/menu';
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||||
|
import { ModeSwitcher } from '@cio/ui/base/dark-mode';
|
||||||
|
|
||||||
import { IconButton } from '$lib/components/IconButton';
|
import { IconButton } from '$lib/components/IconButton';
|
||||||
import { globalStore } from '$lib/utils/store/app';
|
import { globalStore } from '$lib/utils/store/app';
|
||||||
import { currentOrgPath } from '$lib/utils/store/org';
|
import { currentOrgPath } from '$lib/utils/store/org';
|
||||||
import { toggleBodyByMode } from '$lib/utils/functions/app';
|
|
||||||
import { sideBar } from '../Org/store';
|
import { sideBar } from '../Org/store';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
@@ -27,16 +24,6 @@
|
|||||||
$sideBar.hidden = !$sideBar.hidden;
|
$sideBar.hidden = !$sideBar.hidden;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggleDarkMode() {
|
|
||||||
$globalStore.isDark = !$globalStore.isDark;
|
|
||||||
|
|
||||||
toggleBodyByMode($globalStore.isDark);
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
localStorage.setItem('mode', $globalStore.isDark ? 'dark' : '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let coursesPath = $derived(
|
let coursesPath = $derived(
|
||||||
$globalStore.isOrgSite ? '/lms/mylearning' : isCoursePage ? `${$currentOrgPath}/courses` : $currentOrgPath
|
$globalStore.isOrgSite ? '/lms/mylearning' : isCoursePage ? `${$currentOrgPath}/courses` : $currentOrgPath
|
||||||
);
|
);
|
||||||
@@ -91,13 +78,14 @@
|
|||||||
<BellIcon class="custom text-white" size={16} />
|
<BellIcon class="custom text-white" size={16} />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconButton size="small" onClick={toggleDarkMode}>
|
<!-- <IconButton size="small" onClick={toggleDarkMode}>
|
||||||
{#if $globalStore.isDark}
|
{#if $globalStore.isDark}
|
||||||
<SunIcon size={16} class="custom" />
|
<SunIcon size={16} class="custom" />
|
||||||
{:else}
|
{:else}
|
||||||
<MoonIcon class="custom text-white" size={16} />
|
<MoonIcon class="custom text-white" size={16} />
|
||||||
{/if}
|
{/if}
|
||||||
</IconButton>
|
</IconButton> -->
|
||||||
|
<ModeSwitcher />
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li></li>
|
<li></li>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { isCoursePage } from '$lib/utils/functions/app';
|
import { isCoursePage } from '$lib/utils/functions/app';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import type { TCustomLinks } from './types';
|
import type { TCustomLinks } from './types';
|
||||||
|
import { ModeSwitcher } from '@cio/ui/base/dark-mode';
|
||||||
|
|
||||||
import Logo from './Logo.svelte';
|
import Logo from './Logo.svelte';
|
||||||
import CustomLinks from './CustomLinks.svelte';
|
import CustomLinks from './CustomLinks.svelte';
|
||||||
@@ -76,6 +77,8 @@
|
|||||||
{:else if !isOrgSite && !$page.url.pathname?.includes('/404')}
|
{:else if !isOrgSite && !$page.url.pathname?.includes('/404')}
|
||||||
<AuthButtons {disableSignup} {redirect} />
|
<AuthButtons {disableSignup} {redirect} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<ModeSwitcher />
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{#if showLinks}
|
{#if showLinks}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Popover } from 'carbon-components-svelte';
|
|
||||||
import Menu from './Menu.svelte';
|
import Menu from './Menu.svelte';
|
||||||
import Modal from '$lib/components/Modal/index.svelte';
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
|
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
import { profileMenu } from '../store';
|
import { profileMenu } from '../store';
|
||||||
|
import { clickOutside } from '$lib/utils/functions/routes/dashboard';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $isMobile}
|
{#if $isMobile}
|
||||||
@@ -16,15 +17,12 @@
|
|||||||
>
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</Modal>
|
</Modal>
|
||||||
{:else}
|
{:else if $profileMenu.open}
|
||||||
<Popover
|
<div
|
||||||
bind:open={$profileMenu.open}
|
use:clickOutside
|
||||||
align="right-bottom"
|
on:click:outside={() => ($profileMenu.open = false)}
|
||||||
caret={true}
|
class="absolute bottom-28 left-0 z-[300] w-full border bg-black p-2 dark:bg-white"
|
||||||
on:click:outside={({ detail }) => {
|
|
||||||
$profileMenu.open = !!$profileMenu.ref?.contains(detail.target);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</Popover>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -76,10 +76,11 @@
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = error instanceof Error ? error.message : `${error}`;
|
let message = error instanceof Error ? error.message : `${error}`;
|
||||||
|
|
||||||
if (message.includes('profile_username_key')) {
|
if (message.includes('profile_username_key')) {
|
||||||
message = $t('snackbar.lms.error.username_exists');
|
message = $t('snackbar.lms.error.username_exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbar.error(`${$t('snackbar.lms.error.update')}: ${message}`);
|
snackbar.error(`${$t('snackbar.lms.error.update')}: ${message}`);
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||||
import UsersIcon from '@lucide/svelte/icons/users';
|
import UsersIcon from '@lucide/svelte/icons/users';
|
||||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
import Settings2 from '@lucide/svelte/icons/settings-2';
|
||||||
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
|
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
|
||||||
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help';
|
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help';
|
||||||
import Dice6Icon from '@lucide/svelte/icons/dice-6';
|
import Dice6Icon from '@lucide/svelte/icons/dice-6';
|
||||||
@@ -98,24 +98,24 @@
|
|||||||
{#if menuItem.show}
|
{#if menuItem.show}
|
||||||
<a href="{$currentOrgPath}{menuItem.path}" class="text-black no-underline" onclick={toggleSidebar}>
|
<a href="{$currentOrgPath}{menuItem.path}" class="text-black no-underline" onclick={toggleSidebar}>
|
||||||
<li
|
<li
|
||||||
class="mb-1 flex items-center gap-2.5 px-2.5 py-2 {NavClasses.item} {menuItem.isActive
|
class="group mb-1 flex items-center gap-2.5 px-2.5 py-2 {NavClasses.item} {menuItem.isActive
|
||||||
? NavClasses.active
|
? NavClasses.active
|
||||||
: 'dark:text-white'}"
|
: 'dark:text-white'}"
|
||||||
>
|
>
|
||||||
{#if menuItem.path === ''}
|
{#if menuItem.path === ''}
|
||||||
<LayoutDashboardIcon size={16} />
|
<LayoutDashboardIcon size={16} class="group-hover:animate-gather" />
|
||||||
{:else if menuItem.path === '/courses'}
|
{:else if menuItem.path === '/courses'}
|
||||||
<LibraryBigIcon size={16} />
|
<LibraryBigIcon size={16} class="group-hover:animate-library-expand" />
|
||||||
{:else if menuItem.path === '/site'}
|
{:else if menuItem.path === '/site'}
|
||||||
<LayoutTemplateIcon size={16} />
|
<LayoutTemplateIcon size={16} class="group-hover:animate-template-morph" />
|
||||||
{:else if menuItem.path === '/community'}
|
{:else if menuItem.path === '/community'}
|
||||||
<MessageSquareMoreIcon size={16} />
|
<MessageSquareMoreIcon size={16} class="group-hover:animate-ripple" />
|
||||||
{:else if menuItem.path === '/quiz'}
|
{:else if menuItem.path === '/quiz'}
|
||||||
<Dice6Icon size={16} />
|
<Dice6Icon size={16} class="group-hover:animate-roll" />
|
||||||
{:else if menuItem.path === '/audience'}
|
{:else if menuItem.path === '/audience'}
|
||||||
<UsersIcon size={16} />
|
<UsersIcon size={16} class="group-hover:animate-gather" />
|
||||||
{:else if menuItem.path === '/setup'}
|
{:else if menuItem.path === '/setup'}
|
||||||
<SettingsIcon size={16} />
|
<Settings2 size={16} class="group-hover:animate-setup-configure" />
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-sm font-medium">{menuItem.label}</p>
|
<p class="text-sm font-medium">{menuItem.label}</p>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -52,3 +52,19 @@ export const formatDate = (_selectedDate: string | undefined): string => {
|
|||||||
|
|
||||||
return selectedDate.format('ddd, MMM D');
|
return selectedDate.format('ddd, MMM D');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function clickOutside(node) {
|
||||||
|
const handleClick = (event) => {
|
||||||
|
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||||
|
node.dispatchEvent(new CustomEvent('click:outside', { detail: event.target }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClick, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('click', handleClick, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,9 +158,7 @@ export const orgLandingpageValidation = (fields = {}) => {
|
|||||||
export const onboardingValidation = (fields = {}, step) => {
|
export const onboardingValidation = (fields = {}, step) => {
|
||||||
const onboardingValidationSchema = {
|
const onboardingValidationSchema = {
|
||||||
stepOne: z.object({
|
stepOne: z.object({
|
||||||
fullname: z
|
fullname: z.string().min(5, { message: `${t.get('validations.onboarding.step_one.full_name.min_char')}` }),
|
||||||
.string()
|
|
||||||
.min(5, { message: `${t.get('validations.onboarding.step_one.full_name.min_char')}` }),
|
|
||||||
orgName: getOrgNameValidation(),
|
orgName: getOrgNameValidation(),
|
||||||
siteName: getSiteNameValidation()
|
siteName: getSiteNameValidation()
|
||||||
}),
|
}),
|
||||||
@@ -178,8 +176,7 @@ export const onboardingValidation = (fields = {}, step) => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema =
|
const schema = step === 1 ? onboardingValidationSchema.stepOne : onboardingValidationSchema.stepTwo;
|
||||||
step === 1 ? onboardingValidationSchema.stepOne : onboardingValidationSchema.stepTwo;
|
|
||||||
const { error } = schema.safeParse(fields);
|
const { error } = schema.safeParse(fields);
|
||||||
|
|
||||||
return processErrors(error);
|
return processErrors(error);
|
||||||
|
|||||||
@@ -13,7 +13,13 @@
|
|||||||
// import PageLoadProgressBar from '$lib/components/Progress/PageLoadProgressBar.svelte';
|
// import PageLoadProgressBar from '$lib/components/Progress/PageLoadProgressBar.svelte';
|
||||||
import Snackbar from '$lib/components/Snackbar/index.svelte';
|
import Snackbar from '$lib/components/Snackbar/index.svelte';
|
||||||
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
||||||
import { isCoursesPage, isLMSPage, isOrgPage, toggleBodyByMode } from '$lib/utils/functions/app';
|
import {
|
||||||
|
isCoursesPage,
|
||||||
|
isLMSPage,
|
||||||
|
isOrgPage
|
||||||
|
|
||||||
|
// toggleBodyByMode
|
||||||
|
} from '$lib/utils/functions/app';
|
||||||
import { getProfile, setupAnalytics } from '$lib/utils/functions/appSetup';
|
import { getProfile, setupAnalytics } from '$lib/utils/functions/appSetup';
|
||||||
import hideNavByRoute from '$lib/utils/functions/routes/hideNavByRoute';
|
import hideNavByRoute from '$lib/utils/functions/routes/hideNavByRoute';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
@@ -28,7 +34,9 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { MetaTags } from 'svelte-meta-tags';
|
import { MetaTags } from 'svelte-meta-tags';
|
||||||
|
|
||||||
import '../app.postcss';
|
import { ModeWatcher } from '@cio/ui/base/dark-mode';
|
||||||
|
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
let { data, children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
@@ -52,8 +60,8 @@
|
|||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
// Update theme - dark or light mode
|
// Update theme - dark or light mode
|
||||||
$globalStore.isDark = localStorage.getItem('mode') === 'dark';
|
// $globalStore.isDark = localStorage.getItem('mode') === 'dark';
|
||||||
toggleBodyByMode($globalStore.isDark);
|
// toggleBodyByMode($globalStore.isDark);
|
||||||
|
|
||||||
if (data.isOrgSite && data.org?.theme) {
|
if (data.isOrgSite && data.org?.theme) {
|
||||||
setTheme(data.org?.theme);
|
setTheme(data.org?.theme);
|
||||||
@@ -116,9 +124,11 @@
|
|||||||
|
|
||||||
<svelte:window onresize={handleResize} />
|
<svelte:window onresize={handleResize} />
|
||||||
|
|
||||||
|
<ModeWatcher />
|
||||||
|
|
||||||
<MetaTags {...metaTags} />
|
<MetaTags {...metaTags} />
|
||||||
|
|
||||||
<Theme bind:theme={carbonTheme} />
|
<!-- <Theme bind:theme={carbonTheme} /> -->
|
||||||
|
|
||||||
<UpgradeModal />
|
<UpgradeModal />
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
import { validateEmail } from '$lib/utils/functions/validateEmail';
|
import { validateEmail } from '$lib/utils/functions/validateEmail';
|
||||||
|
import Edra from '$lib/components/Edra/EdraRoot.svelte';
|
||||||
|
|
||||||
let email = $state('');
|
let email = $state('');
|
||||||
let isAdding = $state(false);
|
let isAdding = $state(false);
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
<title>ClassroomIO - The classroom software</title>
|
<title>ClassroomIO - The classroom software</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="m-2 flex w-screen flex-col items-center justify-center font-sans sm:m-0 md:h-[93vh]">
|
<div class="m-2 flex w-screen flex-col items-center justify-center font-sans sm:m-0 md:min-h-[93vh] dark:bg-black">
|
||||||
<!--
|
<!--
|
||||||
<img
|
<img
|
||||||
src="/logo-192.png"
|
src="/logo-192.png"
|
||||||
@@ -73,8 +74,9 @@
|
|||||||
<p class="text-center text-lg dark:text-white">The operating system for classroooms of the future 🚀🚀🚀.</p>
|
<p class="text-center text-lg dark:text-white">The operating system for classroooms of the future 🚀🚀🚀.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Edra />
|
||||||
<form onsubmit={preventDefault(handleSubmit)} class="my-4 hidden">
|
<form onsubmit={preventDefault(handleSubmit)} class="my-4 hidden">
|
||||||
<div class="flex flex-col items-center sm:flex-row">
|
<div class="flex hidden flex-col items-center sm:flex-row">
|
||||||
{#if success}
|
{#if success}
|
||||||
<p class="dark:text-white">You have been added successfully. Thanks for joining.</p>
|
<p class="dark:text-white">You have been added successfully. Thanks for joining.</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -91,18 +93,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col md:flex-row">
|
<div class="mt-4 flex md:flex-row">
|
||||||
{#each areas as area, index}
|
{#each areas as area, index}
|
||||||
<div
|
<div
|
||||||
class="active hover:border-primary-700 m-3 max-w-[350px] rounded-md border-2 bg-white px-12 py-3 shadow-xl dark:bg-black {animate}"
|
class="active hover:border-primary-700 m-3 max-w-[350px] rounded-md border-2 bg-white px-12 py-3 shadow-xl dark:bg-black {animate}"
|
||||||
>
|
>
|
||||||
<h3 class="text-3xl dark:text-white">
|
<h3 class="text-3xl dark:text-white">
|
||||||
{#if index === 0}
|
{#if index === 0}
|
||||||
<DoorOpenIcon size={16} />
|
<DoorOpenIcon size={30} />
|
||||||
{:else if index === 1}
|
{:else if index === 1}
|
||||||
<ChartAreaIcon size={16} />
|
<ChartAreaIcon size={30} />
|
||||||
{:else if index === 2}
|
{:else if index === 2}
|
||||||
<BotIcon size={16} />
|
<BotIcon size={30} />
|
||||||
{/if}
|
{/if}
|
||||||
{area.title}
|
{area.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
import { LANGUAGES } from '$lib/utils/constants/translation';
|
import { LANGUAGES } from '$lib/utils/constants/translation';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
import { COURSE_VERSION, type Lesson, type LessonCompletion } from '$lib/utils/types';
|
import { COURSE_VERSION, LOCALE, type Lesson, type LessonCompletion } from '$lib/utils/types';
|
||||||
import { Dropdown } from 'carbon-components-svelte';
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||||
|
|
||||||
let { data = $bindable() } = $props();
|
let { data = $bindable() } = $props();
|
||||||
|
|
||||||
@@ -248,7 +248,20 @@
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dropdown items={LANGUAGES} bind:selectedId={$lesson.locale} class="h-full" />
|
<div>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
{#each LANGUAGES as lang}
|
||||||
|
<DropdownMenu.Item onclick={() => ($lesson.locale = lang.id as LOCALE)}>
|
||||||
|
{lang.text}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</RoleBasedSecurity>
|
</RoleBasedSecurity>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white">
|
||||||
<h3 class="mt-0 text-2xl font-bold">
|
<h3 class="mt-0 text-2xl font-bold">
|
||||||
{$t('dashboard.top_courses')}
|
{$t('dashboard.top_courses')}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white">
|
||||||
<h3 class="mt-0 text-2xl font-bold">
|
<h3 class="mt-0 text-2xl font-bold">
|
||||||
{$t('dashboard.recent_enrollments')}
|
{$t('dashboard.recent_enrollments')}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -12,12 +12,17 @@ const config = {
|
|||||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [vitePreprocess({})],
|
preprocess: [vitePreprocess({})],
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
adapter: useNodeAdapter ? adapterNode() : adapterVercel(),
|
adapter: useNodeAdapter ? adapterNode() : adapterVercel(),
|
||||||
alias: {
|
alias: {
|
||||||
$lib: path.resolve('./src/lib'),
|
$lib: path.resolve('./src/lib'),
|
||||||
$mail: path.resolve('./src/mail')
|
$mail: path.resolve('./src/mail'),
|
||||||
|
'$src/tools': path.resolve('../../packages/ui/src/tools/index.ts'),
|
||||||
|
'$src/base/*': path.resolve('../../packages/ui/src/base/*'),
|
||||||
|
'@cio/ui': path.resolve('../../packages/ui/src'),
|
||||||
|
'@cio/ui/*': path.resolve('../../packages/ui/src/*'),
|
||||||
|
'@cio/api': path.resolve('../api/dist'),
|
||||||
|
'@cio/api/*': path.resolve('../api/dist/*')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
darkMode: 'class',
|
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
|
||||||
theme: {
|
|
||||||
extend: {}
|
|
||||||
},
|
|
||||||
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')]
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
export default ({ mode }) => {
|
export default ({ mode }) => {
|
||||||
@@ -25,15 +24,12 @@ export default ({ mode }) => {
|
|||||||
include: ['@cio/api/rpc-types']
|
include: ['@cio/api/rpc-types']
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
mainFields: ['browser'],
|
mainFields: ['browser']
|
||||||
alias: {
|
|
||||||
'@cio/api': path.resolve(__dirname, '../api/dist')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getServer(params: any) {
|
function getServer(params) {
|
||||||
const { VITE_USE_HTTPS_ON_LOCALHOST } = params || {};
|
const { VITE_USE_HTTPS_ON_LOCALHOST } = params || {};
|
||||||
if (VITE_USE_HTTPS_ON_LOCALHOST === 'true') {
|
if (VITE_USE_HTTPS_ON_LOCALHOST === 'true') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+6
-3
@@ -11,9 +11,11 @@
|
|||||||
"format": "prettier . --write",
|
"format": "prettier . --write",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"ci": "cypress run",
|
"ci": "cypress run",
|
||||||
|
"dashboard:dev": "pnpm dev --filter=@cio/dashboard --filter=@cio/ui",
|
||||||
"dashboard:start": "pnpm start --filter @cio/dashboard",
|
"dashboard:start": "pnpm start --filter @cio/dashboard",
|
||||||
"api:start": "pnpm start --filter @cio/api",
|
"api:start": "pnpm start --filter @cio/api",
|
||||||
"supabase:push": "pnpx supabase link --project-ref $PROJECT_ID && pnpx supabase db push --include-all"
|
"supabase:push": "pnpx supabase link --project-ref $PROJECT_ID && pnpx supabase db push --include-all",
|
||||||
|
"storybook:dev": "pnpm dev --filter=@cio/storybook --filter=@cio/ui"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -34,7 +36,8 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"esbuild"
|
"esbuild",
|
||||||
|
"@tailwindcss/oxide"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/sveltekit';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.stories.@(js|ts|svelte)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
'@storybook/addon-svelte-csf'
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/sveltekit',
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Preview } from '@storybook/svelte';
|
||||||
|
|
||||||
|
import '../src/index.css';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# @cio/storybook
|
||||||
|
|
||||||
|
Storybook for ClassroomIO UI components.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To start the Storybook development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
To build the Storybook:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "@cio/storybook",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "storybook dev -p 6006",
|
||||||
|
"build": "storybook build",
|
||||||
|
"preview": "storybook preview",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cio/ui": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@storybook/addon-essentials": "^8.4.2",
|
||||||
|
"@storybook/addon-interactions": "^8.4.2",
|
||||||
|
"@storybook/addon-links": "^8.4.2",
|
||||||
|
"@storybook/addon-svelte-csf": "^5.0.6",
|
||||||
|
"@storybook/blocks": "^8.4.2",
|
||||||
|
"@storybook/svelte": "^8.4.2",
|
||||||
|
"@storybook/svelte-vite": "^10.0.2",
|
||||||
|
"@storybook/sveltekit": "^8.4.2",
|
||||||
|
"@storybook/test": "^8.4.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"storybook": "^8.4.2",
|
||||||
|
"svelte": "^5.39.11",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"@cio/tsconfig": "workspace:*",
|
||||||
|
"typescript-svelte-plugin": "^0.3.50",
|
||||||
|
"vite": "^7.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { Button } from '@cio/ui/base/button';
|
||||||
|
import { FIELDS } from './fields';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Atom/Button',
|
||||||
|
component: Button,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
controls: {
|
||||||
|
include: FIELDS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template()}
|
||||||
|
<Button>Default Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Secondary">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} variant="secondary">Secondary Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Destructive">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} variant="destructive">Destructive Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Outline">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} variant="outline">Outline Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Ghost">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} variant="ghost">Ghost Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Link">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} variant="link">Link Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Small">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} size="sm">Small Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Large">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} size="lg">Large Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Disabled">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} disabled>Disabled Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="As Link">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Button {...args} href="#">Link Button</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="All Variants">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<Button>Default</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="destructive">Destructive</Button>
|
||||||
|
<Button variant="outline">Outline</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
<Button variant="link">Link</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="All Sizes">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button>Default</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { type ComponentProps } from 'svelte';
|
||||||
|
import { Button } from '@cio/ui/base/button';
|
||||||
|
|
||||||
|
export const FIELDS: (keyof ComponentProps<typeof Button>)[] = ['variant', 'size', 'disabled', 'class', 'type', 'href'];
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { DropdownMenu, Button } from '@cio/ui';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Atom/DropdownMenu',
|
||||||
|
component: DropdownMenu.Root,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template()}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button.Root variant="outline">Open</Button.Root>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Label>My Account</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>Profile</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Billing</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Settings</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Keyboard shortcuts</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item>Team</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger>Invite users</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent>
|
||||||
|
<DropdownMenu.Item>Email</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Message</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>More...</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.Item>New Team</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>GitHub</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Support</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item disabled>API</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>Log out</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Icons">
|
||||||
|
{#snippet template()}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button.Root variant="outline">Open</Button.Root>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<span>👤 Profile</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<span>💳 Billing</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<span>⚙️ Settings</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<span>🚪 Logout</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Simple Menu">
|
||||||
|
{#snippet template()}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button.Root variant="outline">Open</Button.Root>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item>Edit</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Archive</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item class="text-red-600">Delete</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Checkboxes">
|
||||||
|
{#snippet template()}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button.Root variant="outline">Open</Button.Root>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Label>View Options</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.CheckboxItem checked>Status Bar</DropdownMenu.CheckboxItem>
|
||||||
|
<DropdownMenu.CheckboxItem>Activity Bar</DropdownMenu.CheckboxItem>
|
||||||
|
<DropdownMenu.CheckboxItem checked>Panel</DropdownMenu.CheckboxItem>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.RadioGroup value="single">
|
||||||
|
<DropdownMenu.RadioItem value="single">Single</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="group">Group</DropdownMenu.RadioItem>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { type ComponentProps } from 'svelte';
|
||||||
|
import { Input } from '@cio/ui/base/input';
|
||||||
|
|
||||||
|
export const FIELDS: (keyof ComponentProps<typeof Input>)[] = [
|
||||||
|
'value',
|
||||||
|
'type',
|
||||||
|
'placeholder',
|
||||||
|
'disabled',
|
||||||
|
'class',
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'required',
|
||||||
|
'readonly'
|
||||||
|
];
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { Input } from '@cio/ui/base/input';
|
||||||
|
import { Label } from '@cio/ui/base/label';
|
||||||
|
import { FIELDS } from './fields';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Atom/Input',
|
||||||
|
component: Input,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
controls: {
|
||||||
|
include: FIELDS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default" args={{ placeholder: 'Enter text...' }} />
|
||||||
|
|
||||||
|
<Story name="With Label">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||||
|
<Label for="email">Email</Label>
|
||||||
|
<Input type="email" id="email" placeholder="Email" />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Password">
|
||||||
|
{#snippet template()}
|
||||||
|
<Input type="password" placeholder="Password" />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Disabled">
|
||||||
|
{#snippet template()}
|
||||||
|
<Input disabled placeholder="Disabled input" />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Error">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||||
|
<Label for="email-error">Email</Label>
|
||||||
|
<Input type="email" id="email-error" placeholder="Email" class="border-red-500" />
|
||||||
|
<p class="text-sm text-red-500">This field is required</p>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="File Input">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||||
|
<Label for="picture">Picture</Label>
|
||||||
|
<Input id="picture" type="file" />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Search">
|
||||||
|
{#snippet template()}
|
||||||
|
<Input type="search" placeholder="Search..." />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { Popover, Button, Input, Label } from '@cio/ui';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Atom/Popover',
|
||||||
|
component: Popover.Root,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template()}
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<Button.Root variant="outline">Open popover</Button.Root>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="w-80">
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium leading-none">Dimensions</h4>
|
||||||
|
<p class="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="grid grid-cols-3 items-center gap-4">
|
||||||
|
<Label.Root for="width">Width</Label.Root>
|
||||||
|
<Input.Root id="width" value="100%" class="col-span-2 h-8" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 items-center gap-4">
|
||||||
|
<Label.Root for="maxWidth">Max. width</Label.Root>
|
||||||
|
<Input.Root id="maxWidth" value="300px" class="col-span-2 h-8" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 items-center gap-4">
|
||||||
|
<Label.Root for="height">Height</Label.Root>
|
||||||
|
<Input.Root id="height" value="25px" class="col-span-2 h-8" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 items-center gap-4">
|
||||||
|
<Label.Root for="maxHeight">Max. height</Label.Root>
|
||||||
|
<Input.Root id="maxHeight" value="none" class="col-span-2 h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Simple">
|
||||||
|
{#snippet template()}
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<Button.Root>Click me</Button.Root>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content>
|
||||||
|
<p class="text-sm">This is a simple popover with some text content.</p>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Form">
|
||||||
|
{#snippet template()}
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<Button.Root variant="outline">Settings</Button.Root>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="w-80">
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium leading-none">Settings</h4>
|
||||||
|
<p class="text-muted-foreground text-sm">Manage your account settings.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<Label.Root for="name">Display name</Label.Root>
|
||||||
|
<Input.Root id="name" value="Pedro Duarte" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<Label.Root for="username">Username</Label.Root>
|
||||||
|
<Input.Root id="username" value="@peduarte" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button.Root size="sm" class="w-full">Save changes</Button.Root>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Information">
|
||||||
|
{#snippet template()}
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<Button.Root variant="ghost" size="sm">ℹ️ Info</Button.Root>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="w-56">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium">About this feature</h4>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
This feature allows you to quickly access important information without leaving the current page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { type ComponentProps } from 'svelte';
|
||||||
|
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||||
|
|
||||||
|
export const FIELDS: (keyof ComponentProps<typeof Skeleton>)[] = ['class'];
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||||
|
import { FIELDS } from './fields';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Atom/Skeleton',
|
||||||
|
component: Skeleton,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
controls: {
|
||||||
|
include: FIELDS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Skeleton {...args} class="h-4 w-[250px]" />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Circle">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Skeleton {...args} class="h-12 w-12 rounded-full" />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Card">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<Skeleton class="h-12 w-12 rounded-full" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-4 w-[250px]" />
|
||||||
|
<Skeleton class="h-4 w-[200px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Multiple Lines">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-4 w-[350px]" />
|
||||||
|
<Skeleton class="h-4 w-[300px]" />
|
||||||
|
<Skeleton class="h-4 w-[250px]" />
|
||||||
|
<Skeleton class="h-4 w-[200px]" />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Button">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Skeleton {...args} class="h-10 w-20" />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Loading State">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="space-y-4 rounded-lg border p-4">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<Skeleton class="h-12 w-12 rounded-full" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-4 w-[200px]" />
|
||||||
|
<Skeleton class="h-4 w-40" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-[200px] w-full" />
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<Skeleton class="h-8 w-16" />
|
||||||
|
<Skeleton class="h-8 w-16" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
import { Tooltip, Button } from '@cio/ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Button.Root variant="outline">Hover me</Button.Root>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p>Add to library</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { Tooltip, Provider, Trigger, Content } from '@cio/ui/base/tooltip';
|
||||||
|
import { Button } from '@cio/ui/base/button';
|
||||||
|
// import { Arro}
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Atom/Tooltip',
|
||||||
|
component: Provider,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template()}
|
||||||
|
<Provider>
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="outline">Hover me</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>Add to library</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
</Provider>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Arrow">
|
||||||
|
{#snippet template()}
|
||||||
|
<Provider>
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button>Hover for tooltip</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>This tooltip has an arrow pointing to the trigger</p>
|
||||||
|
<!-- <Arrow /> -->
|
||||||
|
=>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
</Provider>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Simple Text">
|
||||||
|
{#snippet template()}
|
||||||
|
<Provider>
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="ghost" size="sm">❓</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>Help information</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
</Provider>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Multiple Tooltips">
|
||||||
|
{#snippet template()}
|
||||||
|
<Provider>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="outline">Save</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>Save your work (Ctrl+S)</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="outline">Edit</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>Edit this item</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="destructive">Delete</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>Permanently delete this item</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Provider>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Icon Button">
|
||||||
|
{#snippet template()}
|
||||||
|
<Provider>
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="ghost" size="sm">🔧</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content>
|
||||||
|
<p>Settings</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
</Provider>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Long Content">
|
||||||
|
{#snippet template()}
|
||||||
|
<Provider>
|
||||||
|
<Tooltip>
|
||||||
|
<Trigger>
|
||||||
|
<Button variant="outline">Info</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content class="max-w-xs">
|
||||||
|
<p>
|
||||||
|
This is a longer tooltip with more detailed information that might span multiple lines to provide
|
||||||
|
comprehensive context about the feature or action.
|
||||||
|
</p>
|
||||||
|
</Content>
|
||||||
|
</Tooltip>
|
||||||
|
</Provider>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@import '@cio/ui/output.css';
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { EditorProps } from '@cio/ui/custom/editor/types';
|
||||||
|
|
||||||
|
export const FIELDS: (keyof EditorProps)[] = [
|
||||||
|
'showToolBar',
|
||||||
|
'editable',
|
||||||
|
'enablePersistence',
|
||||||
|
'contentStorageKey',
|
||||||
|
'editableStorageKey',
|
||||||
|
'class',
|
||||||
|
'editorClass'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_CONTENT = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
attrs: { level: 1 },
|
||||||
|
content: [{ type: 'text', text: 'Welcome to the Editor' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: 'This is a ' },
|
||||||
|
{ type: 'text', marks: [{ type: 'bold' }], text: 'rich text editor' },
|
||||||
|
{ type: 'text', text: ' with support for various formatting options.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bulletList',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'listItem',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'Support for lists' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'listItem',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'Rich text formatting' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'listItem',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'And much more!' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import { FIELDS, DEFAULT_CONTENT } from './constants';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Molecules/Editor',
|
||||||
|
component: Editor,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
controls: {
|
||||||
|
include: FIELDS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Sample Content">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor content={DEFAULT_CONTENT} />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Read Only">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor content={DEFAULT_CONTENT} editable={false} />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Without Toolbar">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor content={DEFAULT_CONTENT} showToolBar={false} />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Persistence">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor content={DEFAULT_CONTENT} enablePersistence={true} contentStorageKey="storybook-editor-content" />
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Custom Styling">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor
|
||||||
|
content={DEFAULT_CONTENT}
|
||||||
|
class="mx-auto max-w-4xl rounded-lg border-2 border-blue-300"
|
||||||
|
editorClass="h-64 p-4 bg-gray-50"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Callbacks">
|
||||||
|
{#snippet template()}
|
||||||
|
<Editor
|
||||||
|
content={DEFAULT_CONTENT}
|
||||||
|
onContentChange={(content) => console.log('Content changed:', content)}
|
||||||
|
onEditorReady={(editor) => console.log('Editor ready:', editor)}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Multiple Editors">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold">Article</h3>
|
||||||
|
<Editor content={DEFAULT_CONTENT} contentStorageKey="article-content" enablePersistence={true} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-lg font-semibold">Notes</h3>
|
||||||
|
<Editor showToolBar={false} editorClass="h-48" contentStorageKey="notes-content" enablePersistence={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"extends": "@cio/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"paths": {
|
||||||
|
"@cio/ui": ["../ui/src"],
|
||||||
|
"@cio/ui/*": ["../ui/src/*"],
|
||||||
|
"$src/*": ["../ui/*"]
|
||||||
|
},
|
||||||
|
"typeRoots": ["../../node_modules/@types", "./node_modules/@types"],
|
||||||
|
"types": ["node"],
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "typescript-svelte-plugin",
|
||||||
|
"enabled": true,
|
||||||
|
"assumeIsSvelteProject": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", ".storybook/**/*", "vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
import * as path from 'path';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), svelte()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@cio/ui': path.resolve(__dirname, '../ui/src'),
|
||||||
|
$src: path.resolve(__dirname, '../ui/src')
|
||||||
|
},
|
||||||
|
conditions: ['svelte', 'browser', 'import']
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['svelte-tiptap'],
|
||||||
|
exclude: ['@tiptap/core', '@tiptap/pm']
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
noExternal: ['svelte-tiptap']
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cio/tsconfig",
|
"name": "@cio/tsconfig",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$src",
|
||||||
|
"utils": "$src/tools/index",
|
||||||
|
"ui": "$src/base",
|
||||||
|
"hooks": "$src/hooks"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "@cio/ui",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"generate-css": "echo 'generating CSS' && pnpx @tailwindcss/cli -i ./src/index.css -o ./src/output.css --minify",
|
||||||
|
"build": "pnpm generate-css",
|
||||||
|
"dev": "pnpm generate-css --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@internationalized/date": "^3.8.1",
|
||||||
|
"@lucide/svelte": "^0.544.0",
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/kit": "^2.5.27",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
|
"@tailwindcss/typography": "^0.5.18",
|
||||||
|
"bits-ui": "^2.11.7",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"svelte": "^5.39.11",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwind-variants": "^3.1.1",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tw-animate-css": "^1.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aarkue/tiptap-math-extension": "^1.4.0",
|
||||||
|
"@floating-ui/dom": "^1.7.4",
|
||||||
|
"@lucide/svelte": "^0.546.0",
|
||||||
|
"@tiptap/core": "^3.7.1",
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.7.1",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.7.1",
|
||||||
|
"@tiptap/extension-highlight": "^3.7.1",
|
||||||
|
"@tiptap/extension-image": "^3.7.1",
|
||||||
|
"@tiptap/extension-list": "^3.7.1",
|
||||||
|
"@tiptap/extension-subscript": "^3.7.1",
|
||||||
|
"@tiptap/extension-superscript": "^3.7.1",
|
||||||
|
"@tiptap/extension-table": "^3.7.1",
|
||||||
|
"@tiptap/extension-text-align": "^3.7.1",
|
||||||
|
"@tiptap/extension-text-style": "^3.7.1",
|
||||||
|
"@tiptap/extension-typography": "^3.7.1",
|
||||||
|
"@tiptap/extensions": "^3.7.1",
|
||||||
|
"@tiptap/pm": "^3.7.1",
|
||||||
|
"@tiptap/starter-kit": "^3.7.1",
|
||||||
|
"@tiptap/suggestion": "^3.7.1",
|
||||||
|
"katex": "^0.16.22",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
|
"mode-watcher": "^0.5.1",
|
||||||
|
"svelte-tiptap": "^3.0.0",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
|
"tiptap-extension-auto-joiner": "^0.1.3",
|
||||||
|
"tiptap-markdown": "^0.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bits-ui": "^2.11.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from '../../tools';
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
|
||||||
|
outline:
|
||||||
|
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
|
icon: 'size-9',
|
||||||
|
'icon-sm': 'size-8',
|
||||||
|
'icon-lg': 'size-10'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = 'button',
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? 'link' : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SunIcon from '@lucide/svelte/icons/sun';
|
||||||
|
import MoonIcon from '@lucide/svelte/icons/moon';
|
||||||
|
|
||||||
|
import { resetMode, setMode } from 'mode-watcher';
|
||||||
|
import * as DropdownMenu from '$src/base/dropdown-menu';
|
||||||
|
import { buttonVariants } from '$src/base/button';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger class={buttonVariants({ variant: 'outline', size: 'icon' })}>
|
||||||
|
<SunIcon class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 !transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<MoonIcon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 !transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span class="sr-only">Toggle theme</span>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align="end">
|
||||||
|
<DropdownMenu.Item onclick={() => setMode('light')}>Light</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => setMode('dark')}>Dark</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => resetMode()}>System</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as ModeSwitcher } from './dark-mode.svelte';
|
||||||
|
|
||||||
|
export * from 'mode-watcher';
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||||
|
import { cn, type WithoutChildrenOrChild } from '../../tools';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked, indeterminate })}
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if indeterminate}
|
||||||
|
<MinusIcon class="size-4" />
|
||||||
|
{:else}
|
||||||
|
<CheckIcon class={cn('size-4', !checked && 'text-transparent')} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.()}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
sideOffset = 4,
|
||||||
|
portalProps,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.ContentProps & {
|
||||||
|
portalProps?: DropdownMenuPrimitive.PortalProps;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Portal {...portalProps}>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
{sideOffset}
|
||||||
|
class={cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-dropdown-menu-content-available-height) origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-group-heading"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
variant = 'default',
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: 'default' | 'destructive';
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '../../tools';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import CircleIcon from '@lucide/svelte/icons/circle';
|
||||||
|
import { cn, type WithoutChild } from '../../tools';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if checked}
|
||||||
|
<CircleIcon class="size-2 fill-current" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.({ checked })}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
|
||||||
|
let { ref = $bindable(null), class: className, ...restProps }: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
class={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '../../tools';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
|
||||||
|
let { ref = $bindable(null), class: className, ...restProps }: DropdownMenuPrimitive.SubContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
class={cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn(
|
||||||
|
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronRightIcon class="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||||
|
import Content from "./dropdown-menu-content.svelte";
|
||||||
|
import Group from "./dropdown-menu-group.svelte";
|
||||||
|
import Item from "./dropdown-menu-item.svelte";
|
||||||
|
import Label from "./dropdown-menu-label.svelte";
|
||||||
|
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||||
|
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||||
|
import Separator from "./dropdown-menu-separator.svelte";
|
||||||
|
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||||
|
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||||
|
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||||
|
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||||
|
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||||
|
const Sub = DropdownMenuPrimitive.Sub;
|
||||||
|
const Root = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
export {
|
||||||
|
CheckboxItem,
|
||||||
|
Content,
|
||||||
|
Root as DropdownMenu,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
GroupHeading as DropdownMenuGroupHeading,
|
||||||
|
Group,
|
||||||
|
GroupHeading,
|
||||||
|
Item,
|
||||||
|
Label,
|
||||||
|
RadioGroup,
|
||||||
|
RadioItem,
|
||||||
|
Root,
|
||||||
|
Separator,
|
||||||
|
Shortcut,
|
||||||
|
Sub,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
Trigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '../../tools';
|
||||||
|
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||||
|
|
||||||
|
type Props = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, 'type'> & ({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
files = $bindable(),
|
||||||
|
class: className,
|
||||||
|
'data-slot': dataSlot = 'input',
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === 'file'}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Label as LabelPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$src/tools/index.js';
|
||||||
|
|
||||||
|
let { ref = $bindable(null), class: className, ...restProps }: LabelPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="label"
|
||||||
|
class={cn(
|
||||||
|
'flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||||
|
import Content from "./popover-content.svelte";
|
||||||
|
import Trigger from "./popover-trigger.svelte";
|
||||||
|
const Root = PopoverPrimitive.Root;
|
||||||
|
const Close = PopoverPrimitive.Close;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Close,
|
||||||
|
//
|
||||||
|
Root as Popover,
|
||||||
|
Content as PopoverContent,
|
||||||
|
Trigger as PopoverTrigger,
|
||||||
|
Close as PopoverClose,
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
align = 'center',
|
||||||
|
portalProps,
|
||||||
|
...restProps
|
||||||
|
}: PopoverPrimitive.ContentProps & {
|
||||||
|
portalProps?: PopoverPrimitive.PortalProps;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Portal {...portalProps}>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="popover-content"
|
||||||
|
{sideOffset}
|
||||||
|
{align}
|
||||||
|
class={cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { ref = $bindable(null), class: className, ...restProps }: PopoverPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Trigger bind:ref data-slot="popover-trigger" class={cn('', className)} {...restProps} />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./skeleton.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Skeleton,
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef, type WithoutChildren } from '../../tools';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="skeleton"
|
||||||
|
class={cn('bg-accent animate-pulse rounded-md', className)}
|
||||||
|
{...restProps}
|
||||||
|
></div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||||
|
import Trigger from "./tooltip-trigger.svelte";
|
||||||
|
import Content from "./tooltip-content.svelte";
|
||||||
|
|
||||||
|
const Root = TooltipPrimitive.Root;
|
||||||
|
const Provider = TooltipPrimitive.Provider;
|
||||||
|
const Portal = TooltipPrimitive.Portal;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
Provider,
|
||||||
|
Portal,
|
||||||
|
//
|
||||||
|
Root as Tooltip,
|
||||||
|
Content as TooltipContent,
|
||||||
|
Trigger as TooltipTrigger,
|
||||||
|
Provider as TooltipProvider,
|
||||||
|
Portal as TooltipPortal,
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '../../tools';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 0,
|
||||||
|
side = 'top',
|
||||||
|
children,
|
||||||
|
arrowClasses,
|
||||||
|
...restProps
|
||||||
|
}: TooltipPrimitive.ContentProps & {
|
||||||
|
arrowClasses?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
{sideOffset}
|
||||||
|
{side}
|
||||||
|
class={cn(
|
||||||
|
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin) z-50 w-fit text-balance rounded-md px-3 py-1.5 text-xs',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<TooltipPrimitive.Arrow>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'bg-primary z-50 size-2.5 rotate-45 rounded-[2px]',
|
||||||
|
'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]',
|
||||||
|
'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]',
|
||||||
|
'data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2',
|
||||||
|
'data-[side=left]:-translate-y-[calc(50%_-_3px)]',
|
||||||
|
arrowClasses
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
></div>
|
||||||
|
{/snippet}
|
||||||
|
</TooltipPrimitive.Arrow>
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# Reusable Editor Component Usage Examples
|
||||||
|
|
||||||
|
The Editor component has been rewritten to be fully reusable with configurable props instead of managing internal state.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import type { Content } from '@tiptap/core';
|
||||||
|
|
||||||
|
let content: Content = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'Hello, world!' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor bind:content />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using HTML Content
|
||||||
|
|
||||||
|
You can also initialize the editor with HTML content by converting it to the appropriate Content format:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import type { Content } from '@tiptap/core';
|
||||||
|
|
||||||
|
// Example with HTML content
|
||||||
|
let htmlContent = `
|
||||||
|
<h1>Welcome to the Editor</h1>
|
||||||
|
<p>This is a <strong>paragraph</strong> with <em>formatting</em>.</p>
|
||||||
|
<ul>
|
||||||
|
<li>First item</li>
|
||||||
|
<li>Second item with <a href="https://example.com">a link</a></li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>This is a quote block</p>
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// The editor will automatically parse HTML content
|
||||||
|
let content: Content = htmlContent;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor bind:content />
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set HTML content programmatically:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import type { Content, Editor as TiptapEditor } from '@tiptap/core';
|
||||||
|
|
||||||
|
let content: Content = { type: 'doc', content: [] };
|
||||||
|
let editor: TiptapEditor;
|
||||||
|
|
||||||
|
function loadHtmlContent() {
|
||||||
|
const htmlString = `
|
||||||
|
<h2>Dynamically Loaded Content</h2>
|
||||||
|
<p>This content was loaded after the editor was ready.</p>
|
||||||
|
<code>console.log('Hello, world!');</code>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.commands.setContent(htmlString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditorReady(editorInstance: TiptapEditor) {
|
||||||
|
editor = editorInstance;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
bind:content
|
||||||
|
onEditorReady={handleEditorReady}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button on:click={loadHtmlContent}>
|
||||||
|
Load HTML Content
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage with All Props
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import type { Content, Editor as TiptapEditor } from '@tiptap/core';
|
||||||
|
|
||||||
|
let content: Content = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'Hello, world!' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let editable = true;
|
||||||
|
let showToolBar = true;
|
||||||
|
|
||||||
|
function handleContentChange(newContent: Content) {
|
||||||
|
console.log('Content changed:', newContent);
|
||||||
|
// Handle content changes
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditorReady(editor: TiptapEditor) {
|
||||||
|
console.log('Editor ready:', editor);
|
||||||
|
// Handle editor ready
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
bind:content
|
||||||
|
bind:editable
|
||||||
|
{showToolBar}
|
||||||
|
enablePersistence={true}
|
||||||
|
contentStorageKey="my-app-content"
|
||||||
|
editableStorageKey="my-app-editable"
|
||||||
|
class="custom-editor-wrapper"
|
||||||
|
editorClass="custom-editor h-96"
|
||||||
|
onContentChange={handleContentChange}
|
||||||
|
onEditorReady={handleEditorReady}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple Editors
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { Editor } from '@cio/ui/custom/editor';
|
||||||
|
import type { Content } from '@tiptap/core';
|
||||||
|
|
||||||
|
let article: Content = { type: 'doc', content: [] };
|
||||||
|
let notes: Content = { type: 'doc', content: [] };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h2>Article</h2>
|
||||||
|
<Editor
|
||||||
|
bind:content={article}
|
||||||
|
contentStorageKey="article-content"
|
||||||
|
enablePersistence={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<Editor
|
||||||
|
bind:content={notes}
|
||||||
|
showToolBar={false}
|
||||||
|
contentStorageKey="notes-content"
|
||||||
|
enablePersistence={true}
|
||||||
|
editorClass="h-48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props Reference
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `content` | `Content` | Default doc with placeholder | Editor content (bindable) |
|
||||||
|
| `showToolBar` | `boolean` | `true` | Whether to show the toolbar |
|
||||||
|
| `editable` | `boolean` | `true` | Whether the editor is editable |
|
||||||
|
| `enablePersistence` | `boolean` | `false` | Enable localStorage persistence |
|
||||||
|
| `contentStorageKey` | `string` | `'edra-content'` | localStorage key for content |
|
||||||
|
| `editableStorageKey` | `string` | `'edra-editable'` | localStorage key for editable state |
|
||||||
|
| `class` | `string` | Default wrapper classes | CSS classes for wrapper |
|
||||||
|
| `editorClass` | `string` | `'h-[32rem] overflow-auto'` | CSS classes for editor |
|
||||||
|
| `onContentChange` | `(content: Content) => void` | `undefined` | Callback when content changes |
|
||||||
|
| `onEditorReady` | `(editor: Editor) => void` | `undefined` | Callback when editor is ready |
|
||||||
|
| `onEditorDestroy` | `() => void` | `undefined` | Callback when editor is destroyed |
|
||||||
|
|
||||||
|
## Callbacks
|
||||||
|
|
||||||
|
| Callback | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `onContentChange` | `(content: Content) => void` | Called when editor content changes |
|
||||||
|
| `onEditorReady` | `(editor: Editor) => void` | Called when editor is ready |
|
||||||
|
| `onEditorDestroy` | `() => void` | Called when editor is destroyed |
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<Editor
|
||||||
|
content={myContent}
|
||||||
|
onContentChange={(content) => {
|
||||||
|
console.log('New content:', content);
|
||||||
|
// Save to database, update state, etc.
|
||||||
|
}}
|
||||||
|
onEditorReady={(editor) => {
|
||||||
|
console.log('Editor is ready, you can focus it:', editor);
|
||||||
|
editor.commands.focus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Version
|
||||||
|
|
||||||
|
### Before (internal state)
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { Editor } from './editor.svelte';
|
||||||
|
// No props needed, everything was internal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor />
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (prop-based)
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { Editor } from './editor.svelte';
|
||||||
|
import type { Content } from '@tiptap/core';
|
||||||
|
|
||||||
|
let content: Content = { type: 'doc', content: [] };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Editor bind:content />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of the Rewrite
|
||||||
|
|
||||||
|
1. **Reusability**: Can be used multiple times with different configurations
|
||||||
|
2. **State Control**: Parent components control the state
|
||||||
|
3. **Flexibility**: Persistence can be disabled or customized
|
||||||
|
4. **Callback Handling**: Modern callback props instead of deprecated events
|
||||||
|
5. **TypeScript Support**: Full type safety with proper interfaces
|
||||||
|
6. **Customization**: All styling can be overridden via props
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ToolBarCommands } from './toolbar-commands';
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
import AlighJustify from '@lucide/svelte/icons/align-justify';
|
||||||
|
import AlignCenter from '@lucide/svelte/icons/align-center';
|
||||||
|
import AlignLeft from '@lucide/svelte/icons/align-left';
|
||||||
|
import AlignRight from '@lucide/svelte/icons/align-right';
|
||||||
|
import Audio from '@lucide/svelte/icons/audio-lines';
|
||||||
|
import Bold from '@lucide/svelte/icons/bold';
|
||||||
|
import Code from '@lucide/svelte/icons/code';
|
||||||
|
import type { EdraToolBarCommands } from './types';
|
||||||
|
import Heading1 from '@lucide/svelte/icons/heading-1';
|
||||||
|
import Heading2 from '@lucide/svelte/icons/heading-2';
|
||||||
|
import Heading3 from '@lucide/svelte/icons/heading-3';
|
||||||
|
import Heading4 from '@lucide/svelte/icons/heading-4';
|
||||||
|
import IFrame from '@lucide/svelte/icons/code-xml';
|
||||||
|
import Image from '@lucide/svelte/icons/image';
|
||||||
|
import Italic from '@lucide/svelte/icons/italic';
|
||||||
|
import Link from '@lucide/svelte/icons/link-2';
|
||||||
|
import List from '@lucide/svelte/icons/list';
|
||||||
|
import ListChecks from '@lucide/svelte/icons/list-checks';
|
||||||
|
import ListOrdered from '@lucide/svelte/icons/list-ordered';
|
||||||
|
import Quote from '@lucide/svelte/icons/quote';
|
||||||
|
import Redo from '@lucide/svelte/icons/redo-2';
|
||||||
|
import StrikeThrough from '@lucide/svelte/icons/strikethrough';
|
||||||
|
import Subscript from '@lucide/svelte/icons/subscript';
|
||||||
|
import Superscript from '@lucide/svelte/icons/superscript';
|
||||||
|
import Table from '@lucide/svelte/icons/table';
|
||||||
|
import Underline from '@lucide/svelte/icons/underline';
|
||||||
|
import Undo from '@lucide/svelte/icons/undo-2';
|
||||||
|
import Video from '@lucide/svelte/icons/video';
|
||||||
|
import { isMac } from '../utils';
|
||||||
|
|
||||||
|
const commands: Record<string, EdraToolBarCommands[]> = {
|
||||||
|
'undo-redo': [
|
||||||
|
{
|
||||||
|
icon: Undo,
|
||||||
|
name: 'undo',
|
||||||
|
tooltip: 'Undo',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}Z`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().undo().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().undo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Redo,
|
||||||
|
name: 'redo',
|
||||||
|
tooltip: 'Redo',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}Y`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().redo().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().redo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
headings: [
|
||||||
|
{
|
||||||
|
icon: Heading1,
|
||||||
|
name: 'h1',
|
||||||
|
tooltip: 'Heading 1',
|
||||||
|
shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}1`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleHeading({ level: 1 }).run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleHeading({ level: 1 });
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('heading', { level: 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Heading2,
|
||||||
|
name: 'h2',
|
||||||
|
tooltip: 'Heading 2',
|
||||||
|
shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}2`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleHeading({ level: 2 }).run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleHeading({ level: 2 });
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('heading', { level: 2 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Heading3,
|
||||||
|
name: 'h3',
|
||||||
|
tooltip: 'Heading 3',
|
||||||
|
shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}3`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleHeading({ level: 3 }).run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleHeading({ level: 3 });
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('heading', { level: 3 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Heading4,
|
||||||
|
name: 'h4',
|
||||||
|
tooltip: 'Heading 4',
|
||||||
|
shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}4`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleHeading({ level: 4 }).run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleHeading({ level: 4 });
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('heading', { level: 4 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'text-formatting': [
|
||||||
|
{
|
||||||
|
icon: Link,
|
||||||
|
name: 'link',
|
||||||
|
tooltip: 'Link',
|
||||||
|
onClick: (editor) => {
|
||||||
|
if (editor.isActive('link')) {
|
||||||
|
editor.chain().focus().unsetLink().run();
|
||||||
|
} else {
|
||||||
|
const url = window.prompt('Enter the URL of the link:');
|
||||||
|
if (url) {
|
||||||
|
editor.chain().focus().toggleLink({ href: url }).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('link');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Bold,
|
||||||
|
name: 'bold',
|
||||||
|
tooltip: 'Bold',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}B`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleBold().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleBold();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('bold');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Italic,
|
||||||
|
name: 'italic',
|
||||||
|
tooltip: 'Italic',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}I`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleItalic().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleItalic();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('italic');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Underline,
|
||||||
|
name: 'underline',
|
||||||
|
tooltip: 'Underline',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}U`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleUnderline().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleUnderline();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('underline');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: StrikeThrough,
|
||||||
|
name: 'strikethrough',
|
||||||
|
tooltip: 'Strikethrough',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}S`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleStrike().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleStrike();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('strike');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Quote,
|
||||||
|
name: 'blockQuote',
|
||||||
|
tooltip: 'BlockQuote',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}B`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleBlockquote().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleBlockquote();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('blockquote');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Code,
|
||||||
|
name: 'code',
|
||||||
|
tooltip: 'Inline Code',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}E`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleCode().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleCode();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('code');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Superscript,
|
||||||
|
name: 'superscript',
|
||||||
|
tooltip: 'Superscript',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'}.`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleSuperscript().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleSuperscript();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('superscript');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Subscript,
|
||||||
|
name: 'subscript',
|
||||||
|
tooltip: 'Subscript',
|
||||||
|
shortCut: `${isMac ? '⌘' : 'Ctrl+'},`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleSubscript().run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleSubscript();
|
||||||
|
},
|
||||||
|
isActive: (editor) => {
|
||||||
|
return editor.isActive('subscript');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
alignment: [
|
||||||
|
{
|
||||||
|
icon: AlignLeft,
|
||||||
|
name: 'align-left',
|
||||||
|
tooltip: 'Align Left',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}L`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleTextAlign('left').run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleTextAlign('left');
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive({ textAlign: 'left' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: AlignCenter,
|
||||||
|
name: 'align-center',
|
||||||
|
tooltip: 'Align Center',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}E`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleTextAlign('center').run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleTextAlign('center');
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive({ textAlign: 'center' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: AlignRight,
|
||||||
|
name: 'align-right',
|
||||||
|
tooltip: 'Align Right',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}R`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleTextAlign('right').run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleTextAlign('right');
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive({ textAlign: 'right' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: AlighJustify,
|
||||||
|
name: 'align-justify',
|
||||||
|
tooltip: 'Align Justify',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}J`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleTextAlign('justify').run();
|
||||||
|
},
|
||||||
|
clickable: (editor) => {
|
||||||
|
return editor.can().toggleTextAlign('justify');
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive({ textAlign: 'justify' })
|
||||||
|
}
|
||||||
|
],
|
||||||
|
lists: [
|
||||||
|
{
|
||||||
|
icon: List,
|
||||||
|
name: 'bulletList',
|
||||||
|
tooltip: 'Bullet List',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}8`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleBulletList().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('bulletList')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ListOrdered,
|
||||||
|
name: 'orderedList',
|
||||||
|
tooltip: 'Ordered List',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}7`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleOrderedList().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('orderedList')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ListChecks,
|
||||||
|
name: 'taskList',
|
||||||
|
tooltip: 'Task List',
|
||||||
|
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}9`,
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().toggleTaskList().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('taskList')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
icon: Image,
|
||||||
|
name: 'image-placeholder',
|
||||||
|
tooltip: 'Image Placeholder',
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().insertImagePlaceholder().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('image-placeholder')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Video,
|
||||||
|
name: 'video-placeholder',
|
||||||
|
tooltip: 'Video Placeholder',
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().insertVideoPlaceholder().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('video-placeholder')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Audio,
|
||||||
|
name: 'audio-placeholder',
|
||||||
|
tooltip: 'Audio Placeholder',
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().insertAudioPlaceholder().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('audio-placeholder')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: IFrame,
|
||||||
|
name: 'iframe-placeholder',
|
||||||
|
tooltip: 'IFrame Placeholder',
|
||||||
|
onClick: (editor) => {
|
||||||
|
editor.chain().focus().insertIFramePlaceholder().run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('iframe-placeholder')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
table: [
|
||||||
|
{
|
||||||
|
icon: Table,
|
||||||
|
name: 'table',
|
||||||
|
tooltip: 'Table',
|
||||||
|
onClick: (editor) => {
|
||||||
|
if (editor.isActive('table')) {
|
||||||
|
const del = confirm('Do you really want to delete this table??');
|
||||||
|
if (del) {
|
||||||
|
editor.chain().focus().deleteTable().run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.chain().focus().insertTable({ cols: 3, rows: 3, withHeaderRow: false }).run();
|
||||||
|
},
|
||||||
|
isActive: (editor) => editor.isActive('table')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default commands;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
import { Icon } from '@lucide/svelte';
|
||||||
|
|
||||||
|
export interface EdraToolBarCommands {
|
||||||
|
name: string;
|
||||||
|
icon: typeof Icon;
|
||||||
|
tooltip?: string;
|
||||||
|
shortCut?: string;
|
||||||
|
onClick?: (editor: Editor) => void;
|
||||||
|
isActive?: (editor: Editor) => boolean;
|
||||||
|
clickable?: (editor: Editor) => boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, type Snippet } from 'svelte';
|
||||||
|
import { BubbleMenuPlugin, type BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu';
|
||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
|
||||||
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
extends Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> {
|
||||||
|
editor?: Editor;
|
||||||
|
children?: Snippet<[]>;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
pluginKey?: string;
|
||||||
|
updateDelay?: number;
|
||||||
|
resizeDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
editor,
|
||||||
|
shouldShow = null,
|
||||||
|
class: className = '',
|
||||||
|
style = '',
|
||||||
|
children,
|
||||||
|
updateDelay,
|
||||||
|
resizeDelay,
|
||||||
|
pluginKey = 'bubbleMenu',
|
||||||
|
options,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let element = $state<HTMLElement>();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
element.style.position = 'absolute';
|
||||||
|
element.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
if (!editor || editor.isDestroyed) {
|
||||||
|
console.warn('BubbleMenu component does not have editor prop or editor is destroyed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = BubbleMenuPlugin({
|
||||||
|
pluginKey,
|
||||||
|
editor,
|
||||||
|
element,
|
||||||
|
shouldShow,
|
||||||
|
updateDelay,
|
||||||
|
resizeDelay,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.registerPlugin(plugin);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (editor && !editor.isDestroyed) {
|
||||||
|
editor.unregisterPlugin(pluginKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={element} class={`bubble-menu-wrapper ${className}`} {style} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import GripVertical from '@lucide/svelte/icons/grip-vertical';
|
||||||
|
import { DragHandlePlugin } from '../extensions/drag-handle';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
editor: Editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { editor }: Props = $props();
|
||||||
|
|
||||||
|
const pluginKey = 'globalDragHandle';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const plugin = DragHandlePlugin({
|
||||||
|
pluginKey: pluginKey,
|
||||||
|
dragHandleWidth: 20,
|
||||||
|
scrollTreshold: 100,
|
||||||
|
dragHandleSelector: '.drag-handle',
|
||||||
|
excludedTags: ['pre', 'code', 'table p'],
|
||||||
|
customNodes: []
|
||||||
|
});
|
||||||
|
editor.registerPlugin(plugin);
|
||||||
|
return () => editor.unregisterPlugin(pluginKey);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="drag-handle">
|
||||||
|
<GripVertical />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Icon } from '@lucide/svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { NodeViewWrapper } from 'svelte-tiptap';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: typeof Icon;
|
||||||
|
title?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
class?: string;
|
||||||
|
children?: Snippet<[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { icon, title, onClick, class: className = '', children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper
|
||||||
|
as="div"
|
||||||
|
contenteditable="false"
|
||||||
|
class={`media-placeholder ${className}`}
|
||||||
|
onclick={onClick}
|
||||||
|
style="user-select: none;"
|
||||||
|
>
|
||||||
|
{#if !children && icon && title}
|
||||||
|
{@const Icon = icon}
|
||||||
|
<Icon />
|
||||||
|
<div contenteditable="false">{title}</div>
|
||||||
|
{/if}
|
||||||
|
{@render children?.()}
|
||||||
|
</NodeViewWrapper>
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
/* Base TipTap Editor Styles with Light/Dark Theme Support */
|
||||||
|
|
||||||
|
.tiptap :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Placeholder */
|
||||||
|
.tiptap .is-empty::before {
|
||||||
|
pointer-events: none;
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
color: var(--border-color-hover);
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heading Styles */
|
||||||
|
.tiptap h1,
|
||||||
|
.tiptap h2,
|
||||||
|
.tiptap h3,
|
||||||
|
.tiptap h4,
|
||||||
|
.tiptap h5,
|
||||||
|
.tiptap h6 {
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap h1,
|
||||||
|
.tiptap h2 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap h1 {
|
||||||
|
scroll-margin: 5rem;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap h4,
|
||||||
|
.tiptap h5,
|
||||||
|
.tiptap h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap blockquote {
|
||||||
|
margin: 1rem 0rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--blockquote-color);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap blockquote::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 0.25rem;
|
||||||
|
background-color: var(--blockquote-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal Rule */
|
||||||
|
.tiptap hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline Code */
|
||||||
|
.tiptap code:not(pre code) {
|
||||||
|
background-color: var(--code-bg);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.2rem 0.3rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List Styling */
|
||||||
|
|
||||||
|
.tiptap ul,
|
||||||
|
.tiptap ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin: 0.5rem 1rem 0.5rem 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul li p,
|
||||||
|
.tiptap ol li p {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task List Styling */
|
||||||
|
.tiptap ul[data-type='taskList'] {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type='taskList'] li {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type='taskList'] li > label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type='taskList'] li > div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type='taskList'] input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap ul[data-type='taskList'] ul[data-type='taskList'] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type='taskList'] li[data-checked='true'] div {
|
||||||
|
color: var(--task-completed-color);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
position: relative;
|
||||||
|
top: 0.25rem;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color Swatches */
|
||||||
|
.color {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color::before {
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
margin-right: 0.1rem;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: var(--color);
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Block Styling */
|
||||||
|
.tiptap pre {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
height: fit-content;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap pre code {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag Handle Styling */
|
||||||
|
.drag-handle {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 50;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 100;
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
color: var(--border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle.hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.drag-handle {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle svg {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Math Equations (KaTeX) */
|
||||||
|
.katex:hover {
|
||||||
|
background-color: var(--code-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.katex.result {
|
||||||
|
border-bottom: 1px dashed var(--highlight-border);
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styling */
|
||||||
|
.ProseMirror .tableWrapper {
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table td,
|
||||||
|
.ProseMirror table th {
|
||||||
|
position: relative;
|
||||||
|
min-width: 100px;
|
||||||
|
border: 1px solid var(--table-border);
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table td:first-of-type:not(a),
|
||||||
|
.ProseMirror table th:first-of-type:not(a) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table td p,
|
||||||
|
.ProseMirror table th p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table td p + p,
|
||||||
|
.ProseMirror table th p + p {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .column-resize-handle {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -0.25rem;
|
||||||
|
bottom: -2px;
|
||||||
|
display: flex;
|
||||||
|
width: 0.5rem;
|
||||||
|
background-color: var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .column-resize-handle::before {
|
||||||
|
content: '';
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
background-color: var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .selectedCell {
|
||||||
|
border-style: double;
|
||||||
|
border-color: var(--table-border);
|
||||||
|
background-color: var(--table-bg-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column,
|
||||||
|
.ProseMirror table .grip-row {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--table-bg-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column {
|
||||||
|
top: -0.75rem;
|
||||||
|
left: 0;
|
||||||
|
margin-left: -1px;
|
||||||
|
height: 0.75rem;
|
||||||
|
width: calc(100% + 1px);
|
||||||
|
border-left: 1px solid var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column:hover::before,
|
||||||
|
.ProseMirror table .grip-column.selected::before {
|
||||||
|
content: '';
|
||||||
|
width: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column:hover {
|
||||||
|
background-color: var(--table-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column:hover::before {
|
||||||
|
border-bottom: 2px dotted var(--border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column.first {
|
||||||
|
border-top-left-radius: 0.125rem;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column.last {
|
||||||
|
border-top-right-radius: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column.selected {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
background-color: var(--table-bg-hover);
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-column.selected::before {
|
||||||
|
border-bottom: 2px dotted var(--border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row {
|
||||||
|
left: -0.75rem;
|
||||||
|
top: 0;
|
||||||
|
margin-top: -1px;
|
||||||
|
height: calc(100% + 1px);
|
||||||
|
width: 0.75rem;
|
||||||
|
border-top: 1px solid var(--table-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row:hover::before,
|
||||||
|
.ProseMirror table .grip-row.selected::before {
|
||||||
|
content: '';
|
||||||
|
height: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row:hover {
|
||||||
|
background-color: var(--table-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row:hover::before {
|
||||||
|
border-left: 2px dotted var(--border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row.first {
|
||||||
|
border-top-left-radius: 0.125rem;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row.last {
|
||||||
|
border-bottom-left-radius: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row.selected {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
background-color: var(--table-bg-hover);
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table .grip-row.selected::before {
|
||||||
|
border-left: 2px dotted var(--border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap .search-result {
|
||||||
|
background-color: var(--search-result-bg);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap .search-result-current {
|
||||||
|
background-color: var(--search-result-current-bg);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-wrapper {
|
||||||
|
background-color: var(--codeblock-bg);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative;
|
||||||
|
height: fit-content;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-wrapper-tile {
|
||||||
|
opacity: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
padding: 0.25rem;
|
||||||
|
right: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-wrapper:hover .code-wrapper-tile {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap iframe {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Content, Editor } from '@tiptap/core';
|
||||||
|
import type { Transaction } from '@tiptap/pm/state';
|
||||||
|
import { EdraEditor, EdraToolBar, EdraBubbleMenu, EdraDragHandleExtended } from './ui';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { cn } from '$src/tools';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// Content of the editor
|
||||||
|
content?: Content;
|
||||||
|
// Whether the toolbar should be visible
|
||||||
|
showToolBar?: boolean;
|
||||||
|
// Whether the editor is editable
|
||||||
|
editable?: boolean;
|
||||||
|
// Whether to enable localStorage persistence
|
||||||
|
enablePersistence?: boolean;
|
||||||
|
// localStorage key for content persistence
|
||||||
|
contentStorageKey?: string;
|
||||||
|
// localStorage key for editable state persistence
|
||||||
|
editableStorageKey?: string;
|
||||||
|
// CSS class for the editor wrapper
|
||||||
|
class?: string;
|
||||||
|
// CSS class for the editor itself
|
||||||
|
editorClass?: string;
|
||||||
|
// Callback functions
|
||||||
|
onContentChange?: (content: Content) => void;
|
||||||
|
onEditorReady?: (editor: Editor) => void;
|
||||||
|
onEditorDestroy?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
content = $bindable(getDefaultContent()),
|
||||||
|
showToolBar = true,
|
||||||
|
editable = true,
|
||||||
|
enablePersistence = false,
|
||||||
|
contentStorageKey = 'edra-content',
|
||||||
|
editableStorageKey = 'edra-editable',
|
||||||
|
class: className = '',
|
||||||
|
editorClass = '',
|
||||||
|
onContentChange,
|
||||||
|
onEditorReady,
|
||||||
|
onEditorDestroy
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let editor = $state<Editor>();
|
||||||
|
|
||||||
|
// Browser detection
|
||||||
|
const browser = typeof window !== 'undefined';
|
||||||
|
|
||||||
|
// Default content fallback
|
||||||
|
function getDefaultContent(): Content {
|
||||||
|
return {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Start typing...'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle content persistence
|
||||||
|
$effect(() => {
|
||||||
|
if (enablePersistence && browser && content) {
|
||||||
|
localStorage.setItem(contentStorageKey, JSON.stringify(content));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle editable state persistence
|
||||||
|
$effect(() => {
|
||||||
|
if (enablePersistence && browser) {
|
||||||
|
localStorage.setItem(editableStorageKey, editable.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load persisted content and editable state on mount
|
||||||
|
$effect(() => {
|
||||||
|
if (enablePersistence && browser) {
|
||||||
|
try {
|
||||||
|
// Load content
|
||||||
|
const rawContentString = localStorage.getItem(contentStorageKey);
|
||||||
|
if (rawContentString !== null) {
|
||||||
|
const persistedContent: Content = JSON.parse(rawContentString);
|
||||||
|
console.log('persistedContent', persistedContent);
|
||||||
|
content = persistedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load editable state
|
||||||
|
const rawEditableString = localStorage.getItem(editableStorageKey);
|
||||||
|
if (rawEditableString !== null) {
|
||||||
|
editable = rawEditableString === 'true';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load persisted state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle editor ready
|
||||||
|
$effect(() => {
|
||||||
|
if (editor) {
|
||||||
|
onEditorReady?.(editor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onUpdate(props: { editor: Editor; transaction: Transaction }) {
|
||||||
|
if (props?.editor && !props.editor.isDestroyed) {
|
||||||
|
const newContent = props.editor.getJSON();
|
||||||
|
content = newContent;
|
||||||
|
onContentChange?.(newContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if browser}
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'bg-background z-50 mx-auto mt-12 flex size-full w-[95%] max-w-5xl flex-col rounded-md border border-dashed sm:w-[85%]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{#if editor && !editor.isDestroyed}
|
||||||
|
{#if showToolBar}
|
||||||
|
<div transition:slide>
|
||||||
|
<EdraToolBar
|
||||||
|
class="bg-secondary/50 flex w-full items-center overflow-x-auto border-b border-dashed p-0.5"
|
||||||
|
{editor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<EdraBubbleMenu {editor} />
|
||||||
|
|
||||||
|
<EdraDragHandleExtended {editor} />
|
||||||
|
{/if}
|
||||||
|
<EdraEditor
|
||||||
|
class={cn('h-[32rem] overflow-auto p-4', editorClass)}
|
||||||
|
bind:editor
|
||||||
|
{editable}
|
||||||
|
content={content || getDefaultContent()}
|
||||||
|
{onUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { Editor, type Extensions, type EditorOptions, type Content } from '@tiptap/core';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import { getHandlePaste } from './utils';
|
||||||
|
import Subscript from '@tiptap/extension-subscript';
|
||||||
|
import Superscript from '@tiptap/extension-superscript';
|
||||||
|
import Typography from '@tiptap/extension-typography';
|
||||||
|
import { ColorHighlighter } from './extensions/ColorHighlighter';
|
||||||
|
import { FontSize, TextStyle, Color } from '@tiptap/extension-text-style';
|
||||||
|
import TextAlign from '@tiptap/extension-text-align';
|
||||||
|
import Highlight from '@tiptap/extension-highlight';
|
||||||
|
import SearchAndReplace from './extensions/FindAndReplace';
|
||||||
|
import { TaskItem, TaskList } from '@tiptap/extension-list';
|
||||||
|
import { Table, TableCell, TableRow, TableHeader } from './extensions/table';
|
||||||
|
import { Placeholder } from '@tiptap/extensions';
|
||||||
|
import { Markdown } from 'tiptap-markdown';
|
||||||
|
import MathExtension from '@aarkue/tiptap-math-extension';
|
||||||
|
import AutoJoiner from 'tiptap-extension-auto-joiner';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
element?: HTMLElement,
|
||||||
|
content?: Content,
|
||||||
|
extensions?: Extensions,
|
||||||
|
options?: Partial<EditorOptions>
|
||||||
|
) => {
|
||||||
|
const editor = new Editor({
|
||||||
|
element,
|
||||||
|
content,
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
orderedList: {
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'list-decimal'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bulletList: {
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'list-disc'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
levels: [1, 2, 3, 4]
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
openOnClick: false,
|
||||||
|
autolink: true,
|
||||||
|
linkOnPaste: true
|
||||||
|
},
|
||||||
|
codeBlock: false
|
||||||
|
}),
|
||||||
|
Highlight.configure({
|
||||||
|
multicolor: true
|
||||||
|
}),
|
||||||
|
Placeholder.configure({
|
||||||
|
emptyEditorClass: 'is-empty',
|
||||||
|
// Use a placeholder:
|
||||||
|
// Use different placeholders depending on the node type:
|
||||||
|
placeholder: ({ node }) => {
|
||||||
|
if (node.type.name === 'heading') {
|
||||||
|
return 'What’s the title?';
|
||||||
|
} else if (node.type.name === 'paragraph') {
|
||||||
|
return 'Press / or write something ...';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Color,
|
||||||
|
Subscript,
|
||||||
|
Superscript,
|
||||||
|
Typography,
|
||||||
|
ColorHighlighter,
|
||||||
|
TextStyle,
|
||||||
|
FontSize,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph']
|
||||||
|
}),
|
||||||
|
TaskList,
|
||||||
|
TaskItem.configure({
|
||||||
|
nested: true
|
||||||
|
}),
|
||||||
|
SearchAndReplace,
|
||||||
|
MathExtension.configure({ evaluation: true }),
|
||||||
|
AutoJoiner,
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Markdown.configure({
|
||||||
|
html: true,
|
||||||
|
tightLists: true,
|
||||||
|
tightListClass: 'tight',
|
||||||
|
bulletListMarker: '-',
|
||||||
|
linkify: true,
|
||||||
|
breaks: true,
|
||||||
|
transformPastedText: true,
|
||||||
|
transformCopiedText: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
...(extensions ?? [])
|
||||||
|
],
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.setOptions({
|
||||||
|
editorProps: {
|
||||||
|
handlePaste: getHandlePaste(editor)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Extension } from '@tiptap/core';
|
||||||
|
import { Plugin } from '@tiptap/pm/state';
|
||||||
|
import { findColors } from '../utils';
|
||||||
|
|
||||||
|
export const ColorHighlighter = Extension.create({
|
||||||
|
name: 'colorHighlighter',
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
state: {
|
||||||
|
init(_, { doc }) {
|
||||||
|
return findColors(doc);
|
||||||
|
},
|
||||||
|
apply(transaction, oldState) {
|
||||||
|
return transaction.docChanged ? findColors(transaction.doc) : oldState;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
return this.getState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
// MIT License
|
||||||
|
|
||||||
|
// Copyright (c) 2023 - 2024 Jeet Mandaliya (Github Username: sereneinserenade)
|
||||||
|
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
import { Extension, type Range, type Dispatch } from '@tiptap/core';
|
||||||
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||||
|
import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/pm/state';
|
||||||
|
import { Node as PMNode } from '@tiptap/pm/model';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
search: {
|
||||||
|
/**
|
||||||
|
* @description Set search term in extension.
|
||||||
|
*/
|
||||||
|
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Set replace term in extension.
|
||||||
|
*/
|
||||||
|
setReplaceTerm: (replaceTerm: string) => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Set case sensitivity in extension.
|
||||||
|
*/
|
||||||
|
setCaseSensitive: (caseSensitive: boolean) => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Reset current search result to first instance.
|
||||||
|
*/
|
||||||
|
resetIndex: () => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Find next instance of search result.
|
||||||
|
*/
|
||||||
|
nextSearchResult: () => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Find previous instance of search result.
|
||||||
|
*/
|
||||||
|
previousSearchResult: () => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Replace first instance of search result with given replace term.
|
||||||
|
*/
|
||||||
|
replace: () => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Replace all instances of search result with given replace term.
|
||||||
|
*/
|
||||||
|
replaceAll: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextNodesWithPosition {
|
||||||
|
text: string;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => {
|
||||||
|
return RegExp(
|
||||||
|
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s,
|
||||||
|
caseSensitive ? 'gu' : 'gui'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProcessedSearches {
|
||||||
|
decorationsToReturn: DecorationSet;
|
||||||
|
results: Range[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSearches(
|
||||||
|
doc: PMNode,
|
||||||
|
searchTerm: RegExp,
|
||||||
|
searchResultClass: string,
|
||||||
|
resultIndex: number
|
||||||
|
): ProcessedSearches {
|
||||||
|
const decorations: Decoration[] = [];
|
||||||
|
const results: Range[] = [];
|
||||||
|
|
||||||
|
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
return {
|
||||||
|
decorationsToReturn: DecorationSet.empty,
|
||||||
|
results: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
doc?.descendants((node, pos) => {
|
||||||
|
if (node.isText) {
|
||||||
|
if (textNodesWithPosition[index]) {
|
||||||
|
textNodesWithPosition[index] = {
|
||||||
|
text: textNodesWithPosition[index].text + node.text,
|
||||||
|
pos: textNodesWithPosition[index].pos
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
textNodesWithPosition[index] = {
|
||||||
|
text: `${node.text}`,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||||
|
|
||||||
|
for (const element of textNodesWithPosition) {
|
||||||
|
const { text, pos } = element;
|
||||||
|
const matches = Array.from(text.matchAll(searchTerm)).filter(([matchText]) => matchText.trim());
|
||||||
|
|
||||||
|
for (const m of matches) {
|
||||||
|
if (m[0] === '') break;
|
||||||
|
|
||||||
|
if (m.index !== undefined) {
|
||||||
|
results.push({
|
||||||
|
from: pos + m.index,
|
||||||
|
to: pos + m.index + m[0].length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i += 1) {
|
||||||
|
const r = results[i];
|
||||||
|
const className =
|
||||||
|
i === resultIndex ? `${searchResultClass} ${searchResultClass}-current` : searchResultClass;
|
||||||
|
const decoration: Decoration = Decoration.inline(r.from, r.to, {
|
||||||
|
class: className
|
||||||
|
});
|
||||||
|
|
||||||
|
decorations.push(decoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||||
|
results
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const replace = (
|
||||||
|
replaceTerm: string,
|
||||||
|
results: Range[],
|
||||||
|
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||||
|
) => {
|
||||||
|
const firstResult = results[0];
|
||||||
|
|
||||||
|
if (!firstResult) return;
|
||||||
|
|
||||||
|
const { from, to } = results[0];
|
||||||
|
|
||||||
|
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||||
|
};
|
||||||
|
|
||||||
|
const rebaseNextResult = (
|
||||||
|
replaceTerm: string,
|
||||||
|
index: number,
|
||||||
|
lastOffset: number,
|
||||||
|
results: Range[]
|
||||||
|
): [number, Range[]] | null => {
|
||||||
|
const nextIndex = index + 1;
|
||||||
|
|
||||||
|
if (!results[nextIndex]) return null;
|
||||||
|
|
||||||
|
const { from: currentFrom, to: currentTo } = results[index];
|
||||||
|
|
||||||
|
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||||
|
|
||||||
|
const { from, to } = results[nextIndex];
|
||||||
|
|
||||||
|
results[nextIndex] = {
|
||||||
|
to: to - offset,
|
||||||
|
from: from - offset
|
||||||
|
};
|
||||||
|
|
||||||
|
return [offset, results];
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceAll = (
|
||||||
|
replaceTerm: string,
|
||||||
|
results: Range[],
|
||||||
|
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||||
|
) => {
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
let resultsCopy = results.slice();
|
||||||
|
|
||||||
|
if (!resultsCopy.length) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||||
|
const { from, to } = resultsCopy[i];
|
||||||
|
|
||||||
|
tr.insertText(replaceTerm, from, to);
|
||||||
|
|
||||||
|
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||||
|
|
||||||
|
if (!rebaseNextResultResponse) continue;
|
||||||
|
|
||||||
|
offset = rebaseNextResultResponse[0];
|
||||||
|
resultsCopy = rebaseNextResultResponse[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dispatch) {
|
||||||
|
dispatch(tr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchAndReplacePluginKey = new PluginKey('searchAndReplacePlugin');
|
||||||
|
|
||||||
|
export interface SearchAndReplaceOptions {
|
||||||
|
searchResultClass: string;
|
||||||
|
disableRegex: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAndReplaceStorage {
|
||||||
|
searchTerm: string;
|
||||||
|
replaceTerm: string;
|
||||||
|
results: Range[];
|
||||||
|
lastSearchTerm: string;
|
||||||
|
caseSensitive: boolean;
|
||||||
|
lastCaseSensitive: boolean;
|
||||||
|
resultIndex: number;
|
||||||
|
lastResultIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchAndReplace = Extension.create<SearchAndReplaceOptions, SearchAndReplaceStorage>({
|
||||||
|
name: 'searchAndReplace',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
searchResultClass: 'search-result',
|
||||||
|
disableRegex: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addStorage() {
|
||||||
|
return {
|
||||||
|
searchTerm: '',
|
||||||
|
replaceTerm: '',
|
||||||
|
results: [],
|
||||||
|
lastSearchTerm: '',
|
||||||
|
caseSensitive: false,
|
||||||
|
lastCaseSensitive: false,
|
||||||
|
resultIndex: 0,
|
||||||
|
lastResultIndex: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setSearchTerm:
|
||||||
|
(searchTerm: string) =>
|
||||||
|
({ editor }) => {
|
||||||
|
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
setReplaceTerm:
|
||||||
|
(replaceTerm: string) =>
|
||||||
|
({ editor }) => {
|
||||||
|
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
setCaseSensitive:
|
||||||
|
(caseSensitive: boolean) =>
|
||||||
|
({ editor }) => {
|
||||||
|
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
resetIndex:
|
||||||
|
() =>
|
||||||
|
({ editor }) => {
|
||||||
|
editor.storage.searchAndReplace.resultIndex = 0;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
nextSearchResult:
|
||||||
|
() =>
|
||||||
|
({ editor }) => {
|
||||||
|
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||||
|
|
||||||
|
const nextIndex = resultIndex + 1;
|
||||||
|
|
||||||
|
if (results[nextIndex]) {
|
||||||
|
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||||
|
} else {
|
||||||
|
editor.storage.searchAndReplace.resultIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
previousSearchResult:
|
||||||
|
() =>
|
||||||
|
({ editor }) => {
|
||||||
|
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||||
|
|
||||||
|
const prevIndex = resultIndex - 1;
|
||||||
|
|
||||||
|
if (results[prevIndex]) {
|
||||||
|
editor.storage.searchAndReplace.resultIndex = prevIndex;
|
||||||
|
} else {
|
||||||
|
editor.storage.searchAndReplace.resultIndex = results.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
replace:
|
||||||
|
() =>
|
||||||
|
({ editor, state, dispatch }) => {
|
||||||
|
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||||
|
|
||||||
|
replace(replaceTerm, results, { state, dispatch });
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
replaceAll:
|
||||||
|
() =>
|
||||||
|
({ editor, tr, dispatch }) => {
|
||||||
|
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||||
|
|
||||||
|
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const editor = this.editor;
|
||||||
|
const { searchResultClass, disableRegex } = this.options;
|
||||||
|
|
||||||
|
const setLastSearchTerm = (t: string) => (editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||||
|
const setLastCaseSensitive = (t: boolean) =>
|
||||||
|
(editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||||
|
const setLastResultIndex = (t: number) => (editor.storage.searchAndReplace.lastResultIndex = t);
|
||||||
|
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: searchAndReplacePluginKey,
|
||||||
|
state: {
|
||||||
|
init: () => DecorationSet.empty,
|
||||||
|
apply({ doc, docChanged }, oldState) {
|
||||||
|
const {
|
||||||
|
searchTerm,
|
||||||
|
lastSearchTerm,
|
||||||
|
caseSensitive,
|
||||||
|
lastCaseSensitive,
|
||||||
|
resultIndex,
|
||||||
|
lastResultIndex
|
||||||
|
} = editor.storage.searchAndReplace;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!docChanged &&
|
||||||
|
lastSearchTerm === searchTerm &&
|
||||||
|
lastCaseSensitive === caseSensitive &&
|
||||||
|
lastResultIndex === resultIndex
|
||||||
|
)
|
||||||
|
return oldState;
|
||||||
|
|
||||||
|
setLastSearchTerm(searchTerm);
|
||||||
|
setLastCaseSensitive(caseSensitive);
|
||||||
|
setLastResultIndex(resultIndex);
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
editor.storage.searchAndReplace.results = [];
|
||||||
|
return DecorationSet.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { decorationsToReturn, results } = processSearches(
|
||||||
|
doc,
|
||||||
|
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||||
|
searchResultClass,
|
||||||
|
resultIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.storage.searchAndReplace.results = results;
|
||||||
|
|
||||||
|
return decorationsToReturn;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
return this.getState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SearchAndReplace;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Audio } from './AudioExtension';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
import type { NodeViewProps } from '@tiptap/core';
|
||||||
|
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||||
|
|
||||||
|
export const AudioExtended = (content: Component<NodeViewProps>) =>
|
||||||
|
Audio.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: '100%'
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
default: 'left'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView: () => {
|
||||||
|
return SvelteNodeViewRenderer(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { Node, nodeInputRule } from '@tiptap/core';
|
||||||
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||||
|
|
||||||
|
export interface AudioOptions {
|
||||||
|
HTMLAttributes: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
audio: {
|
||||||
|
/**
|
||||||
|
* Set a audio node
|
||||||
|
*/
|
||||||
|
setAudio: (src: string) => ReturnType;
|
||||||
|
/**
|
||||||
|
* Toggle a audio
|
||||||
|
*/
|
||||||
|
toggleAudio: (src: string) => ReturnType;
|
||||||
|
/**
|
||||||
|
* Remove a audio
|
||||||
|
*/
|
||||||
|
removeAudio: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUDIO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
|
||||||
|
|
||||||
|
export const Audio = Node.create<AudioOptions>({
|
||||||
|
name: 'audio',
|
||||||
|
group: 'block',
|
||||||
|
draggable: true,
|
||||||
|
isolating: true,
|
||||||
|
atom: true,
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||||
|
renderHTML: (attrs) => ({ src: attrs.src })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'audio',
|
||||||
|
getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') })
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'audio',
|
||||||
|
{ controls: 'true', style: 'width: 100%;', ...HTMLAttributes },
|
||||||
|
['source', HTMLAttributes]
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setAudio:
|
||||||
|
(src: string) =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.insertContent(
|
||||||
|
`<audio controls autoplay="false" style="width: 100%;" src="${src}"></audio>`
|
||||||
|
),
|
||||||
|
|
||||||
|
toggleAudio:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleNode(this.name, 'paragraph'),
|
||||||
|
removeAudio:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.deleteNode(this.name)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
nodeInputRule({
|
||||||
|
find: AUDIO_INPUT_REGEX,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => {
|
||||||
|
const [, , src] = match;
|
||||||
|
|
||||||
|
return { src };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('audioDropPlugin'),
|
||||||
|
|
||||||
|
props: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
drop(view, event) {
|
||||||
|
const {
|
||||||
|
state: { schema, tr },
|
||||||
|
dispatch
|
||||||
|
} = view;
|
||||||
|
const hasFiles =
|
||||||
|
event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||||
|
|
||||||
|
if (!hasFiles) return false;
|
||||||
|
|
||||||
|
const audios = Array.from(event.dataTransfer.files).filter((file) =>
|
||||||
|
/audio/i.test(file.type)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (audios.length === 0) return false;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||||
|
|
||||||
|
audios.forEach((audio) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (readerEvent) => {
|
||||||
|
const node = schema.nodes.audio.create({ src: readerEvent.target?.result });
|
||||||
|
|
||||||
|
if (coordinates && typeof coordinates.pos === 'number') {
|
||||||
|
const transaction = tr.insert(coordinates?.pos, node);
|
||||||
|
|
||||||
|
dispatch(transaction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(audio);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Editor, Node, mergeAttributes, type CommandProps, type NodeViewProps } from '@tiptap/core';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||||
|
|
||||||
|
export interface AudioPlaceholderOptions {
|
||||||
|
HTMLAttributes: Record<string, object>;
|
||||||
|
onDrop: (files: File[], editor: Editor) => void;
|
||||||
|
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||||
|
onEmbed: (url: string, editor: Editor) => void;
|
||||||
|
allowedMimeTypes?: Record<string, string[]>;
|
||||||
|
maxFiles?: number;
|
||||||
|
maxSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
audioPlaceholder: {
|
||||||
|
/**
|
||||||
|
* Inserts an audio placeholder
|
||||||
|
*/
|
||||||
|
insertAudioPlaceholder: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AudioPlaceholder = (
|
||||||
|
component: Component<NodeViewProps>
|
||||||
|
): Node<AudioPlaceholderOptions> =>
|
||||||
|
Node.create<AudioPlaceholderOptions>({
|
||||||
|
name: 'audio-placeholder',
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
onDrop: () => {},
|
||||||
|
onDropRejected: () => {},
|
||||||
|
onEmbed: () => {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['div', mergeAttributes(HTMLAttributes)];
|
||||||
|
},
|
||||||
|
group: 'block',
|
||||||
|
draggable: true,
|
||||||
|
atom: true,
|
||||||
|
content: 'inline*',
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return SvelteNodeViewRenderer(component);
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertAudioPlaceholder: () => (props: CommandProps) => {
|
||||||
|
return props.commands.insertContent({
|
||||||
|
type: 'audio-placeholder'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user