generated from Klectr/KTemplate
Compare commits
29 Commits
5b0b8c2224
...
3d61f28aa0
Author | SHA1 | Date | |
---|---|---|---|
|
3d61f28aa0 | ||
|
c364996229 | ||
a7522db83d | |||
44e7154084 | |||
fd1d55de5d | |||
ae17593cfb | |||
1071743290 | |||
3fd6aef52c | |||
261eb8348e | |||
77624aae8c | |||
f659a38026 | |||
4ea03e68bf | |||
412ae1c740 | |||
640e5af6fe | |||
80e5f03877 | |||
6d4d825d5f | |||
f40fb009f3 | |||
4e86455c5d | |||
3bc92ecb7d | |||
4507e1fe8e | |||
22918a8d72 | |||
f67649b6b7 | |||
0ef4517993 | |||
81b7531f89 | |||
22d5c3ed4e | |||
|
6bbed4100a | ||
a09724b38a | |||
|
2ed58cbee6 | ||
|
4dcea1a42a |
11
index.html
11
index.html
@ -4,11 +4,18 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>KlectrTemplate</title>
|
||||
<title>Kslab</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://unpkg.com/tiny-markdown-editor/dist/tiny-mde.min.css"
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" />
|
||||
<div id="toast-root"></div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.ts" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
18
package.json
18
package.json
@ -1,25 +1,35 @@
|
||||
{
|
||||
"name": "ktemplate",
|
||||
"private": true,
|
||||
"name": "k-slab",
|
||||
"private": false,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"commit": "cz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1",
|
||||
"kaioken": "^0.32.1"
|
||||
"caniuse-lite": "^1.0.30001667",
|
||||
"kaioken": "^0.32.1",
|
||||
"tiny-markdown-editor": "^0.1.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-kaioken": "^0.13.1"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
src-tauri/Cargo.lock
generated
20
src-tauri/Cargo.lock
generated
@ -2,16 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "KTemplate"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.1"
|
||||
@ -1406,6 +1396,16 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k-slab"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.2"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "KTemplate"
|
||||
name = "k-slab"
|
||||
version = "1.0.0"
|
||||
description = "A Kaioken Tauri App Template"
|
||||
description = "A Canvas for your notes"
|
||||
authors = ["tristonarmstrong"]
|
||||
edition = "2021"
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "KlectrTemplate",
|
||||
"productName": "Kslab",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
@ -20,7 +20,7 @@
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "KlectrTemplate",
|
||||
"title": "Kslab",
|
||||
"width": 500,
|
||||
"height": 500
|
||||
}
|
||||
@ -31,7 +31,7 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.klectr-template.dev",
|
||||
"identifier": "com.k-slab.dev",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
10
src/App.tsx
10
src/App.tsx
@ -1,5 +1,13 @@
|
||||
import InfiniteCanvas from "./components/InfinateCanvas"
|
||||
import { ToastContextProvider } from "./components/Toast"
|
||||
import { useThemeDetector } from "./utils/useThemeDetector"
|
||||
|
||||
export function App() {
|
||||
return <InfiniteCanvas />
|
||||
useThemeDetector()
|
||||
|
||||
return (
|
||||
<ToastContextProvider>
|
||||
<InfiniteCanvas />
|
||||
</ToastContextProvider>
|
||||
)
|
||||
}
|
||||
|
BIN
src/assets/fonts/Virgil.woff2
Normal file
BIN
src/assets/fonts/Virgil.woff2
Normal file
Binary file not shown.
7
src/assets/logo.svg
Normal file
7
src/assets/logo.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 91.72003297955337 117.95906943548752" width="183.44006595910673" height="235.91813887097504">
|
||||
<!-- svg-source:excalidraw -->
|
||||
|
||||
<defs>
|
||||
<style class="style-fonts"></style>
|
||||
</defs>
|
||||
<g transform="translate(25.743421873560465 23.41106307747745) rotate(0 0.5830896990207748 39.35855468390125)" stroke="none"><path fill="#e03131" d="M 3.83,0 Q 3.83,0 3.91,3.01 4.00,6.03 4.10,8.82 4.20,11.61 4.05,15.65 3.90,19.68 3.73,24.59 3.55,29.49 3.43,34.92 3.30,40.36 3.21,45.18 3.12,50.01 3.22,54.52 3.32,59.03 3.69,62.24 4.07,65.46 4.37,67.90 4.67,70.34 5.01,74.50 5.35,78.66 5.25,79.33 5.14,80.00 4.83,80.61 4.52,81.21 4.04,81.68 3.55,82.15 2.94,82.44 2.33,82.73 1.65,82.81 0.98,82.90 0.32,82.76 -0.33,82.62 -0.92,82.28 -1.50,81.93 -1.95,81.42 -2.39,80.91 -2.65,80.28 -2.91,79.66 -2.95,78.98 -3.00,78.31 -2.82,77.65 -2.65,77.00 -2.27,76.43 -1.90,75.87 -1.37,75.45 -0.83,75.04 -0.19,74.81 0.44,74.59 1.11,74.58 1.79,74.57 2.44,74.78 3.08,74.99 3.62,75.40 4.17,75.80 4.55,76.36 4.94,76.91 5.13,77.56 5.32,78.21 5.29,78.89 5.26,79.57 5.02,80.20 4.77,80.83 4.34,81.35 3.91,81.87 3.33,82.23 2.75,82.58 2.09,82.74 1.43,82.89 0.76,82.82 0.09,82.76 -0.52,82.48 -1.14,82.20 -1.64,81.74 -2.13,81.28 -2.46,80.69 -2.78,80.09 -2.90,79.43 -3.02,78.76 -3.02,78.76 -3.02,78.76 -2.83,74.71 -2.64,70.65 -2.54,68.29 -2.44,65.94 -2.54,62.59 -2.64,59.25 -2.88,54.63 -3.12,50.01 -3.21,45.18 -3.30,40.36 -3.43,34.92 -3.55,29.49 -3.73,24.59 -3.90,19.68 -4.05,15.65 -4.20,11.61 -4.10,8.82 -4.00,6.03 -3.91,3.01 -3.83,0 -3.77,-0.45 -3.72,-0.91 -3.55,-1.34 -3.39,-1.78 -3.13,-2.16 -2.86,-2.54 -2.52,-2.84 -2.17,-3.15 -1.76,-3.36 -1.35,-3.58 -0.91,-3.69 -0.46,-3.80 0.00,-3.80 0.46,-3.80 0.91,-3.69 1.35,-3.58 1.76,-3.36 2.17,-3.15 2.52,-2.84 2.86,-2.54 3.13,-2.16 3.39,-1.78 3.55,-1.34 3.72,-0.91 3.77,-0.45 3.83,0.00 3.83,0.00 L 3.83,0 Z"></path></g><g transform="translate(66.55970080501362 10) rotate(0 -28.27985040250681 26.530581305444542)" stroke="none"><path fill="#e03131" d="M 2.18,3.32 Q 2.18,3.32 -4.28,7.36 -10.74,11.40 -16.72,16.30 -22.69,21.20 -27.94,26.09 -33.19,30.99 -37.08,35.18 -40.96,39.36 -44.00,42.67 -47.04,45.97 -48.82,48.33 -50.61,50.69 -52.14,52.98 -53.67,55.27 -54.10,55.68 -54.53,56.08 -55.06,56.32 -55.60,56.57 -56.18,56.63 -56.77,56.69 -57.34,56.56 -57.92,56.43 -58.42,56.13 -58.93,55.82 -59.30,55.37 -59.68,54.92 -59.90,54.37 -60.11,53.82 -60.14,53.23 -60.17,52.64 -60.01,52.08 -59.85,51.51 -59.52,51.02 -59.19,50.54 -58.71,50.18 -58.24,49.83 -57.68,49.65 -57.13,49.46 -56.54,49.46 -55.95,49.47 -55.39,49.66 -54.83,49.85 -54.37,50.21 -53.90,50.57 -53.57,51.06 -53.24,51.54 -53.09,52.11 -52.93,52.68 -52.97,53.27 -53.00,53.86 -53.22,54.40 -53.44,54.95 -53.83,55.40 -54.21,55.84 -54.72,56.14 -55.22,56.45 -55.80,56.57 -56.38,56.69 -56.96,56.63 -57.55,56.56 -58.08,56.31 -58.61,56.06 -59.04,55.65 -59.46,55.24 -59.74,54.72 -60.01,54.20 -60.10,53.62 -60.20,53.04 -60.10,52.46 -60.00,51.88 -59.72,51.36 -59.44,50.84 -59.44,50.84 -59.44,50.84 -57.51,48.62 -55.58,46.41 -53.47,44.25 -51.36,42.09 -48.39,38.71 -45.42,35.32 -41.49,30.88 -37.55,26.43 -32.29,21.26 -27.02,16.09 -20.87,10.72 -14.73,5.36 -8.45,1.01 -2.18,-3.32 -1.75,-3.53 -1.32,-3.75 -0.85,-3.85 -0.39,-3.95 0.08,-3.94 0.56,-3.93 1.03,-3.81 1.49,-3.68 1.91,-3.45 2.33,-3.22 2.68,-2.89 3.03,-2.57 3.29,-2.17 3.56,-1.77 3.72,-1.31 3.88,-0.86 3.93,-0.38 3.97,0.08 3.90,0.56 3.84,1.03 3.66,1.48 3.48,1.92 3.19,2.31 2.91,2.70 2.55,3.01 2.18,3.32 2.18,3.32 L 2.18,3.32 Z"></path></g><g transform="translate(29.241960067685056 46.73465103830782) rotate(0 26.239036455934155 30.612209198589852)" stroke="none"><path fill="#e03131" d="M 2.79,-2.00 Q 2.79,-2.00 7.39,4.59 12.00,11.19 18.55,19.12 25.10,27.06 29.71,32.29 34.32,37.53 38.27,41.76 42.22,46.00 45.58,49.68 48.94,53.35 51.74,56.32 54.54,59.29 54.79,59.67 55.05,60.05 55.17,60.49 55.28,60.93 55.26,61.39 55.23,61.85 55.06,62.27 54.88,62.69 54.59,63.04 54.29,63.39 53.89,63.62 53.50,63.85 53.05,63.95 52.61,64.04 52.15,63.99 51.70,63.94 51.28,63.74 50.87,63.55 50.54,63.23 50.21,62.91 50.00,62.51 49.79,62.10 49.72,61.65 49.65,61.20 49.72,60.75 49.80,60.30 50.02,59.90 50.24,59.49 50.57,59.18 50.90,58.87 51.32,58.68 51.74,58.49 52.19,58.44 52.65,58.40 53.09,58.50 53.54,58.60 53.93,58.84 54.32,59.08 54.61,59.43 54.91,59.78 55.07,60.21 55.24,60.63 55.26,61.09 55.28,61.55 55.15,61.98 55.03,62.42 54.77,62.80 54.51,63.18 54.14,63.45 53.78,63.73 53.34,63.87 52.91,64.01 52.45,64.01 52.00,64.00 51.56,63.86 51.13,63.71 50.77,63.43 50.41,63.15 50.41,63.15 50.41,63.15 47.63,60.15 44.85,57.15 41.46,53.53 38.07,49.92 34.00,45.69 29.94,41.45 25.18,36.22 20.42,30.99 13.59,22.97 6.77,14.95 1.98,8.47 -2.79,2.00 -2.99,1.64 -3.19,1.27 -3.29,0.87 -3.40,0.47 -3.41,0.06 -3.42,-0.35 -3.32,-0.75 -3.23,-1.16 -3.05,-1.53 -2.86,-1.90 -2.59,-2.21 -2.32,-2.53 -1.99,-2.77 -1.65,-3.01 -1.26,-3.16 -0.88,-3.32 -0.47,-3.38 -0.06,-3.43 0.34,-3.39 0.76,-3.35 1.15,-3.21 1.54,-3.07 1.88,-2.84 2.23,-2.61 2.51,-2.31 2.79,-2.00 2.79,-2.00 L 2.79,-2.00 Z"></path></g></svg>
|
After Width: | Height: | Size: 4.9 KiB |
7
src/components/Divider.tsx
Normal file
7
src/components/Divider.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export function Divider() {
|
||||
return (
|
||||
<div className="border-l-[1px] dark:border-l-[#5c5c5c] border-l-[#9c9c9c]"></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { useDebounce } from "../utils/useDebounce"
|
||||
import { LayerEnum } from "../utils/enums"
|
||||
import images, { ImageCardType } from "../signals/images"
|
||||
import { updateLocalStorage } from "../utils/localStorage"
|
||||
import { isTheme } from "../utils/isTheme"
|
||||
import { useThemeDetector } from "../utils/useThemeDetector"
|
||||
|
||||
namespace ImageCard {
|
||||
export interface ImageCardProps {
|
||||
@ -121,6 +121,7 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps
|
||||
function ExpandIcon({ cb }: {
|
||||
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined
|
||||
}) {
|
||||
const isDarkTheme = useThemeDetector()
|
||||
return (
|
||||
<svg
|
||||
onmousedown={cb}
|
||||
@ -129,7 +130,7 @@ function ExpandIcon({ cb }: {
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={isTheme('dark') ? "#777" : "#999"}
|
||||
stroke={isDarkTheme ? "#777" : "#999"}
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
@ -1,41 +1,55 @@
|
||||
import { useRef, useEffect } from "kaioken"
|
||||
import { ImagesSignal, NotesSigal, canvasDimentsion } from "../signals"
|
||||
import { ImagesSignal, NotesSigal, TextSignal, canvasDimentsion } from "../signals"
|
||||
import { NoteCard } from "./NoteCard"
|
||||
import notes from "../signals/notes"
|
||||
import { MiniMap } from "./MiniMap"
|
||||
import { ImageCard } from "./ImageCard"
|
||||
import images from "../signals/images"
|
||||
import { CardSelector } from "./cardSelector/CardSelector"
|
||||
import { Logo } from "./Logo"
|
||||
import { useThemeDetector } from "../utils/useThemeDetector"
|
||||
import { isTheme } from "../utils/isTheme"
|
||||
import { TextItem } from "./TextItem"
|
||||
import texts from "../signals/texts"
|
||||
|
||||
export default function InfiniteCanvas() {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isDarkTheme = useThemeDetector()
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({
|
||||
left: (canvasDimentsion.value.width / 2) - (window.innerWidth / 2),
|
||||
top: (canvasDimentsion.value.height / 2) - (window.innerHeight / 2)
|
||||
})
|
||||
const initPos = getInitialPosition(canvasDimentsion)
|
||||
window.scrollTo(initPos)
|
||||
|
||||
const updateDimensions = () => {
|
||||
const _updateDimensions = () => {
|
||||
canvasDimentsion.value = {
|
||||
width: Math.max(canvasDimentsion.value.width, window.innerWidth),
|
||||
height: Math.max(canvasDimentsion.value.height, window.innerHeight),
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions()
|
||||
window.addEventListener("resize", updateDimensions)
|
||||
function _updatePosition() {
|
||||
localStorage.setItem("pos", JSON.stringify({
|
||||
left: window.scrollX,
|
||||
top: window.scrollY
|
||||
}))
|
||||
}
|
||||
|
||||
_updateDimensions()
|
||||
window.addEventListener("resize", _updateDimensions)
|
||||
window.addEventListener("scrollend", _updatePosition)
|
||||
notes.loadLocalStorage()
|
||||
images.loadLocalStorage()
|
||||
texts.loadLocalStorage()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateDimensions)
|
||||
window.removeEventListener("resize", _updateDimensions)
|
||||
window.removeEventListener("scrollend", _updatePosition)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Logo />
|
||||
<CardSelector />
|
||||
<MiniMap />
|
||||
|
||||
@ -49,7 +63,7 @@ export default function InfiniteCanvas() {
|
||||
width: `${canvasDimentsion.value.width}px`,
|
||||
height: `${canvasDimentsion.value.width}px`,
|
||||
backgroundSize: "30px 30px",
|
||||
backgroundImage: `radial-gradient(circle, rgba(${isTheme('dark') ? '255, 255, 255, 0.2' : '0, 0, 0, 0.2'}) 1px, transparent 1px)`,
|
||||
backgroundImage: `radial-gradient(circle, rgba(${isDarkTheme ? '255, 255, 255, 0.2' : '0, 0, 0, 0.2'}) 1px, transparent 1px)`,
|
||||
}}>
|
||||
{Object.keys(NotesSigal.default.notes.value).map((itemKey: string) => {
|
||||
const item = NotesSigal.default.notes.value[itemKey]
|
||||
@ -64,8 +78,45 @@ export default function InfiniteCanvas() {
|
||||
<ImageCard key={itemKey} data={item} />
|
||||
)
|
||||
})}
|
||||
|
||||
{Object.keys(TextSignal.default.texts.value).map((itemKey: string) => {
|
||||
const item = TextSignal.default.texts.value[itemKey]
|
||||
return (
|
||||
<TextItem key={itemKey} data={item} />
|
||||
)
|
||||
})}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface ScrollToOptions extends ScrollOptions {
|
||||
left?: number;
|
||||
top?: number;
|
||||
}
|
||||
|
||||
function getInitialPosition(canvasDimensions: typeof canvasDimentsion): ScrollToOptions {
|
||||
const defaultScroll: ScrollToOptions = {
|
||||
left: (canvasDimensions.value.width / 2) - (window.innerWidth / 2),
|
||||
top: (canvasDimensions.value.height / 2) - (window.innerHeight / 2)
|
||||
}
|
||||
|
||||
let initPosition;
|
||||
try {
|
||||
initPosition = JSON.parse(localStorage.getItem("pos") ?? "")
|
||||
} catch (e) {
|
||||
console.error("no local storage for pos")
|
||||
}
|
||||
|
||||
if (!initPosition) return defaultScroll
|
||||
return initPosition
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
9
src/components/Logo.tsx
Normal file
9
src/components/Logo.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import LogoSvg from '../assets/logo.svg'
|
||||
export function Logo() {
|
||||
return (
|
||||
<>
|
||||
<img width={30} height={30} src={LogoSvg} className={'fixed top-2 left-2 z-[999]'} />
|
||||
<p className={'font-[Virgil] fixed top-3 left-7 dark:text-white z-[999]'}>slab</p>
|
||||
</>
|
||||
)
|
||||
}
|
30
src/components/MarkDownEditor/MarkDownEditor.tsx
Normal file
30
src/components/MarkDownEditor/MarkDownEditor.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Editor, Listener } from 'tiny-markdown-editor'
|
||||
import './md.css'
|
||||
import { useEffect, useRef } from "kaioken"
|
||||
|
||||
namespace MarkDownEditor {
|
||||
export interface Props {
|
||||
initial: string
|
||||
onChange: Listener<'change'>
|
||||
}
|
||||
}
|
||||
|
||||
export function MarkDownEditor({ initial, onChange }: MarkDownEditor.Props) {
|
||||
const elRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!elRef.current) return
|
||||
const editor = new Editor({
|
||||
element: elRef.current,
|
||||
content: initial
|
||||
})
|
||||
editor.addEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'h-full overflow-hidden'}
|
||||
ref={elRef}
|
||||
></div>
|
||||
)
|
||||
}
|
47
src/components/MarkDownEditor/md.css
Normal file
47
src/components/MarkDownEditor/md.css
Normal file
@ -0,0 +1,47 @@
|
||||
.TinyMDE {
|
||||
@apply h-full bg-transparent overflow-y-scroll dark:text-gray-300 text-gray-600;
|
||||
}
|
||||
|
||||
.TMMark_TMH1,
|
||||
.TMMark_TMH2,
|
||||
.TMMark_TMH3,
|
||||
.TMMark_TMH4,
|
||||
.TMMark_TMH5,
|
||||
.TMMark_TMH6,
|
||||
.TMMark_TMOL,
|
||||
.TMMark_TMUL {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
|
||||
.TMH1,
|
||||
.TMH2,
|
||||
.TMH3,
|
||||
.TMH4,
|
||||
.TMH5,
|
||||
.TMH6 {
|
||||
@apply uppercase;
|
||||
}
|
||||
|
||||
.TMLink {
|
||||
@apply decoration-blue-500;
|
||||
}
|
||||
|
||||
.TMImage {
|
||||
@apply decoration-green-500;
|
||||
}
|
||||
|
||||
.TMCode {
|
||||
@apply text-blue-800 px-1 dark:bg-[#ffffff26] dark:text-blue-200 dark:border-[#545454];
|
||||
}
|
||||
|
||||
.TMBlockquote {
|
||||
@apply border-blue-500 bg-blue-100 dark:bg-opacity-5;
|
||||
}
|
||||
|
||||
.TMFencedCodeBacktick {
|
||||
@apply dark:bg-white dark:bg-opacity-10 px-1;
|
||||
}
|
||||
|
||||
.TMInfoString {
|
||||
@apply text-blue-500;
|
||||
}
|
@ -3,6 +3,7 @@ import notes, { NoteCardType } from "../signals/notes"
|
||||
import { canvasDimentsion } from "../signals"
|
||||
import { LayerEnum } from "../utils/enums"
|
||||
import images, { ImageCardType } from "../signals/images"
|
||||
import texts from "../signals/texts"
|
||||
|
||||
const _MAP_OFFSET = 20
|
||||
const _MAP_SCALE_FACTOR = 10
|
||||
@ -21,6 +22,7 @@ export function MiniMap() {
|
||||
const width = canvasDimentsion.value.width / _MAP_SCALE_FACTOR
|
||||
const height = canvasDimentsion.value.height / _MAP_SCALE_FACTOR
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
function _handleScroll(_e: Event) {
|
||||
scrollX.value = window.scrollX
|
||||
@ -31,8 +33,6 @@ export function MiniMap() {
|
||||
return () => window.removeEventListener('scroll', _handleScroll)
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dark:bg-[#ffffff11] bg-[#0001] fixed rounded"
|
||||
@ -48,22 +48,31 @@ export function MiniMap() {
|
||||
const el = useRef(null)
|
||||
|
||||
function _handleItemClick(_e: MouseEvent) {
|
||||
const newLeft = image.position.x - ((viewportWidth / 2) - (image.dimensions.w / 2))
|
||||
const newTop = image.position.y - ((viewportHeight / 2) - (image.dimensions.h / 2))
|
||||
|
||||
window.scrollTo({
|
||||
left: image.position.x - ((viewportWidth / 2) - (image.dimensions.w / 2)),
|
||||
top: image.position.y - ((viewportHeight / 2) - (image.dimensions.h / 2)),
|
||||
left: newLeft,
|
||||
top: newTop,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
const newWidth = image.dimensions.w / _MAP_SCALE_FACTOR
|
||||
const newHeight = image.dimensions.h / _MAP_SCALE_FACTOR
|
||||
const newTop = (image.position.y / _MAP_SCALE_FACTOR)
|
||||
const newLeft = (image.position.x / _MAP_SCALE_FACTOR)
|
||||
const newZIndex = LayerEnum.MINIMAP + 1
|
||||
|
||||
return (
|
||||
<div ref={el} className={"absolute dark:bg-green-500 bg-green-300 dark:hover:bg-blue-500 hover:bg-blue-300 cursor-pointer border dark:border-[#222] border-green-500 rounded"}
|
||||
onclick={_handleItemClick}
|
||||
style={{
|
||||
width: `${image.dimensions.w / _MAP_SCALE_FACTOR}px`,
|
||||
height: `${image.dimensions.h / _MAP_SCALE_FACTOR}px`,
|
||||
top: `${(image.position.y / _MAP_SCALE_FACTOR)}px`,
|
||||
left: `${(image.position.x / _MAP_SCALE_FACTOR)}px`,
|
||||
zIndex: `${LayerEnum.MINIMAP + 1}`
|
||||
width: `${newWidth}px`,
|
||||
height: `${newHeight}px`,
|
||||
top: `${newTop}px`,
|
||||
left: `${newLeft}px`,
|
||||
zIndex: `${newZIndex}`
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
@ -74,21 +83,58 @@ export function MiniMap() {
|
||||
const note = notes.notes.value[noteKey]
|
||||
|
||||
function _handleItemClick(_e: MouseEvent) {
|
||||
const newLeft = note.position.x - ((viewportWidth / 2) - (note.dimensions.w / 2))
|
||||
const newTop = note.position.y - ((viewportHeight / 2) - (note.dimensions.h / 2))
|
||||
window.scrollTo({
|
||||
left: note.position.x - ((viewportWidth / 2) - (note.dimensions.w / 2)),
|
||||
top: note.position.y - ((viewportHeight / 2) - (note.dimensions.h / 2)),
|
||||
left: newLeft,
|
||||
top: newTop,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
const newWidth = note.dimensions.w / _MAP_SCALE_FACTOR
|
||||
const newHeight = note.dimensions.h / _MAP_SCALE_FACTOR
|
||||
const newTop = (note.position.y / _MAP_SCALE_FACTOR)
|
||||
const newLeft = (note.position.x / _MAP_SCALE_FACTOR)
|
||||
const newZIndex = LayerEnum.MINIMAP + 1
|
||||
|
||||
return (
|
||||
<div className={"absolute dark:bg-gray-500 bg-gray-300 hover:bg-blue-500 cursor-pointer border dark:border-[#222] border-gray-400 rounded"}
|
||||
onclick={_handleItemClick}
|
||||
style={{
|
||||
width: `${newWidth}px`,
|
||||
height: `${newHeight}px`,
|
||||
top: `${newTop}px`,
|
||||
left: `${newLeft}px`,
|
||||
zIndex: `${newZIndex}`
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
})}
|
||||
|
||||
{Object.keys(texts.texts.value).map((textKey: textCardType['id']) => {
|
||||
const text = texts.texts.value[textKey]
|
||||
const el = useRef(null)
|
||||
|
||||
function _handleItemClick(_e: MouseEvent) {
|
||||
window.scrollTo({
|
||||
left: text.position.x - ((viewportWidth / 2) - (text.dimensions.w / 2)),
|
||||
top: text.position.y - ((viewportHeight / 2) - (text.dimensions.h / 2)),
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"absolute dark:bg-gray-500 bg-gray-300 hover:bg-blue-500 cursor-pointer border dark:border-[#222] border-gray-500 rounded"}
|
||||
<div ref={el} className={"bg-indigo-500 hover:bg-blue-500 cursor-pointer"}
|
||||
onclick={_handleItemClick}
|
||||
style={{
|
||||
width: `${note.dimensions.w / _MAP_SCALE_FACTOR}px`,
|
||||
height: `${note.dimensions.h / _MAP_SCALE_FACTOR}px`,
|
||||
top: `${(note.position.y / _MAP_SCALE_FACTOR)}px`,
|
||||
left: `${(note.position.x / _MAP_SCALE_FACTOR)}px`,
|
||||
position: 'absolute',
|
||||
width: `${text.dimensions.w / _MAP_SCALE_FACTOR}px`,
|
||||
height: `${text.dimensions.h / _MAP_SCALE_FACTOR}px`,
|
||||
top: `${(text.position.y / _MAP_SCALE_FACTOR)}px`,
|
||||
left: `${(text.position.x / _MAP_SCALE_FACTOR)}px`,
|
||||
border: '1px solid #222',
|
||||
borderRadius: '2px',
|
||||
zIndex: `${LayerEnum.MINIMAP + 1}`
|
||||
}}
|
||||
></div>
|
||||
@ -96,7 +142,7 @@ export function MiniMap() {
|
||||
})}
|
||||
|
||||
<div
|
||||
className={'absolute bg-blue-200 bg-opacity-10 border dark:border-blue-800 border-blue-500 bg-blue-500 rounded'}
|
||||
className={'pointer-events-none absolute bg-blue-200 bg-opacity-10 border dark:border-blue-800 border-blue-500 bg-blue-500 rounded'}
|
||||
style={{
|
||||
width: `${viewportWidth / _MAP_SCALE_FACTOR}px`,
|
||||
height: `${viewportHeight / _MAP_SCALE_FACTOR}px`,
|
||||
|
@ -3,7 +3,12 @@ import { NotesSigal, focusedItem } from "../signals"
|
||||
import { useDebounce } from "../utils/useDebounce"
|
||||
import notes, { NoteCardType } from "../signals/notes"
|
||||
import { LayerEnum } from "../utils/enums"
|
||||
import { isTheme } from "../utils/isTheme"
|
||||
import { useThemeDetector } from "../utils/useThemeDetector"
|
||||
import { MarkDownEditor } from "./MarkDownEditor/MarkDownEditor"
|
||||
import { ChangeEvent } from "tiny-markdown-editor"
|
||||
import { Divider } from "./Divider"
|
||||
import { ExportIcon } from "./icons/ExportIcon"
|
||||
import { createFileAndExport } from "../utils/createFileAndExport"
|
||||
|
||||
namespace NoteCard {
|
||||
export interface NoteCardProps {
|
||||
@ -27,6 +32,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
||||
function updateLocalStorage(time?: number) {
|
||||
debounce(() => {
|
||||
localStorage.setItem("notes", JSON.stringify(notes.notes.value))
|
||||
saved.value = true
|
||||
}, time)
|
||||
}
|
||||
|
||||
@ -86,45 +92,71 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
||||
window.removeEventListener('mouseup', _handleResizeMouseUp)
|
||||
}
|
||||
|
||||
function _handleMdChange(e: ChangeEvent) {
|
||||
NotesSigal.default.updateNoteProperty(itemKey, 'contents', e.content)
|
||||
NotesSigal.default.notes.notify()
|
||||
updateLocalStorage()
|
||||
saved.value = false
|
||||
}
|
||||
|
||||
function _handleClose(_e: Event) {
|
||||
NotesSigal.default.removeNote(item.id)
|
||||
NotesSigal.default.notes.notify()
|
||||
updateLocalStorage()
|
||||
}
|
||||
|
||||
function _handleFocusCard() {
|
||||
focusedItem.value = itemKey
|
||||
}
|
||||
|
||||
function _handleExportClick(_e: MouseEvent) {
|
||||
createFileAndExport("Note", item.contents, "text/markdown")
|
||||
}
|
||||
|
||||
const cardPositionStyle = {
|
||||
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
||||
width: `${item.dimensions.w}px`,
|
||||
height: `${item.dimensions.h}px`,
|
||||
top: `${item.position.y}px`,
|
||||
left: `${item.position.x}px`,
|
||||
}
|
||||
|
||||
const saveIndicatorStyle = {
|
||||
opacity: saved.value ? '0' : '100'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onmousedown={() => focusedItem.value = itemKey}
|
||||
className="text-[#333] dark:bg-[#111] dark:border-[#1c1c1c] bg-[#eee] select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#ddd] absolute"
|
||||
style={{
|
||||
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
||||
width: `${item.dimensions.w}px`,
|
||||
height: `${item.dimensions.h}px`,
|
||||
top: `${item.position.y}px`,
|
||||
left: `${item.position.x}px`,
|
||||
}}
|
||||
onmousedown={_handleFocusCard}
|
||||
style={cardPositionStyle}
|
||||
className="overflow-hidden text-[#333] dark:bg-[#1a1a1a] dark:border-[#1c1c1c] bg-[#eee] select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#ddd] absolute"
|
||||
>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<div className="overflow-hidden flex-1 flex flex-col gap-1">
|
||||
{/* Header Bar */}
|
||||
<div className="px-2 flex justify-between items-center cursor-move" onmousedown={_handleMouseDown}>
|
||||
<div style={{
|
||||
opacity: saved.value ? '0' : '100'
|
||||
}} className={`rounded-full w-1 h-1 dark:bg-white bg-green-500`}></div>
|
||||
<button className="text-md dark:text-[#777] text-black" onclick={(_e: Event) => {
|
||||
NotesSigal.default.removeNote(item.id)
|
||||
NotesSigal.default.notes.notify()
|
||||
updateLocalStorage()
|
||||
}}>x</button>
|
||||
<div style={saveIndicatorStyle} className="rounded-full w-1 h-1 dark:bg-white bg-green-500"></div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
onclick={_handleExportClick}
|
||||
className="flex items-center">
|
||||
<ExportIcon
|
||||
className="dark:text-[#5c5c5c] cursor-pointer w-4 h-4 text-[#9c9c9c] hover:text-blue-500 transition-color duration-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<button className="text-md dark:text-[#777] text-black" onclick={_handleClose}>x</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<hr className="border dark:border-[#1c1c1c] border-[#ddd]" />
|
||||
<textarea
|
||||
placeholder={"Todo: put some note here"}
|
||||
className="flex resize-none px-2 w-full h-full bg-transparent resize-none focus:outline-none dark:text-gray-300"
|
||||
value={item.contents}
|
||||
onkeypress={() => { saved.value = false }}
|
||||
onchange={(e) => {
|
||||
NotesSigal.default.updateNoteProperty(itemKey, 'contents', e.target.value)
|
||||
NotesSigal.default.notes.notify()
|
||||
updateLocalStorage()
|
||||
saved.value = true
|
||||
}}
|
||||
/>
|
||||
|
||||
<hr className="border dark:border-[#2c2c2c] border-[#ddd]" />
|
||||
|
||||
{/* Content Body */}
|
||||
<MarkDownEditor initial={item.contents} onChange={_handleMdChange} />
|
||||
<ExpandIcon cb={_handleResizeMouseDown} />
|
||||
|
||||
</div>
|
||||
</div >
|
||||
|
||||
@ -134,6 +166,8 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
||||
function ExpandIcon({ cb }: {
|
||||
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined
|
||||
}) {
|
||||
const isDarkTheme = useThemeDetector()
|
||||
|
||||
return (
|
||||
<svg
|
||||
onmousedown={cb}
|
||||
@ -142,7 +176,7 @@ function ExpandIcon({ cb }: {
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={isTheme('dark') ? "#777" : "#999"}
|
||||
stroke={isDarkTheme ? "#777" : "#999"}
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@ -154,3 +188,4 @@ function ExpandIcon({ cb }: {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
108
src/components/TextItem.tsx
Normal file
108
src/components/TextItem.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { signal, useRef } from "kaioken"
|
||||
import { TextSignal, focusedItem } from "../signals"
|
||||
import { useDebounce } from "../utils/useDebounce"
|
||||
import texts, { TextCardType } from "../signals/texts"
|
||||
import { LayerEnum } from "../utils/enums"
|
||||
import { isTheme } from "../utils/isTheme"
|
||||
|
||||
namespace TextItem {
|
||||
export interface TextCardProps {
|
||||
key: TextCardType['id']
|
||||
data: TextCardType
|
||||
}
|
||||
}
|
||||
|
||||
export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
|
||||
const saved = signal(true)
|
||||
const pressed = signal(false)
|
||||
const newX = useRef(0)
|
||||
const newY = useRef(0)
|
||||
const offsetX = useRef(0)
|
||||
const offsetY = useRef(0)
|
||||
const initialResizeX = useRef(0)
|
||||
const initialResizeY = useRef(0)
|
||||
|
||||
const { debounce } = useDebounce()
|
||||
|
||||
function updateLocalStorage(time?: number) {
|
||||
debounce(() => {
|
||||
localStorage.setItem("texts", JSON.stringify(texts.texts.value))
|
||||
}, time)
|
||||
}
|
||||
|
||||
function _handleMouseMove(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
if (!pressed.value) return
|
||||
|
||||
newX.current = e.pageX - offsetX.current
|
||||
newY.current = e.pageY - offsetY.current
|
||||
const newPos = { x: newX.current, y: newY.current }
|
||||
|
||||
TextSignal.default.updateTextProperty(itemKey, 'position', newPos)
|
||||
updateLocalStorage()
|
||||
}
|
||||
|
||||
function _handleMouseUp(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
pressed.value = false
|
||||
window.removeEventListener('mousemove', _handleMouseMove)
|
||||
window.removeEventListener('mouseup', _handleMouseUp)
|
||||
}
|
||||
|
||||
function _handleMouseDown(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
offsetX.current = e.offsetX
|
||||
offsetY.current = e.offsetY
|
||||
pressed.value = true
|
||||
window.addEventListener('mousemove', _handleMouseMove)
|
||||
window.addEventListener('mouseup', _handleMouseUp)
|
||||
}
|
||||
|
||||
function _handleResizeMove(e: MouseEvent) {
|
||||
const { pageX, pageY } = e
|
||||
const [newX, newY] = [initialResizeX.current - pageX, initialResizeY.current - pageY]
|
||||
|
||||
const newW = -newX + item.dimensions.w
|
||||
const newH = -newY + item.dimensions.h
|
||||
const newDim = { w: newW, h: newH }
|
||||
|
||||
TextSignal.default.updateTextProperty(itemKey, 'dimensions', newDim)
|
||||
TextSignal.default.texts.notify()
|
||||
}
|
||||
|
||||
|
||||
function _handleResizeMouseDown(e: MouseEvent) {
|
||||
initialResizeX.current = e.pageX
|
||||
initialResizeY.current = e.pageY
|
||||
pressed.value = true
|
||||
window.addEventListener('mousemove', _handleResizeMove)
|
||||
window.addEventListener('mouseup', _handleResizeMouseUp)
|
||||
}
|
||||
|
||||
function _handleResizeMouseUp() {
|
||||
pressed.value = false
|
||||
updateLocalStorage()
|
||||
window.removeEventListener('mousemove', _handleResizeMove)
|
||||
window.removeEventListener('mouseup', _handleResizeMouseUp)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onmousedown={_handleMouseDown}
|
||||
className="select-none transition flex flex-col justify-stretch rounded border border-[#3c3c3c] absolute border-dashed"
|
||||
style={{
|
||||
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
||||
width: `${item.dimensions.w}px`,
|
||||
height: `${item.dimensions.h + 100}px`,
|
||||
top: `${item.position.y}px`,
|
||||
left: `${item.position.x}px`,
|
||||
}}
|
||||
>
|
||||
<div className={'relative w-full h-full'}>
|
||||
<p className={'w-full h-full'}>{item.contents}</p>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
)
|
||||
}
|
||||
|
112
src/components/Toast.tsx
Normal file
112
src/components/Toast.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
Portal,
|
||||
Transition,
|
||||
TransitionState,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "kaioken"
|
||||
|
||||
type Toast = {
|
||||
ts: number
|
||||
type: "info" | "success" | "warning" | "error"
|
||||
message: string
|
||||
expires: number
|
||||
expired?: boolean
|
||||
}
|
||||
|
||||
const defaultDuration = 3000
|
||||
|
||||
const ToastContext = createContext<{
|
||||
showToast: (type: Toast["type"], message: string) => void
|
||||
}>(null as any)
|
||||
|
||||
export const useToast = () => useContext(ToastContext)
|
||||
|
||||
export const ToastContextProvider: Kaioken.FC = ({ children }) => {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setToasts((prev) =>
|
||||
prev.map((toast) => {
|
||||
if (toast.expired) {
|
||||
return toast
|
||||
}
|
||||
return {
|
||||
...toast,
|
||||
expired: Date.now() > toast.expires,
|
||||
}
|
||||
})
|
||||
)
|
||||
}, 500)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const showToast = (
|
||||
type: Toast["type"],
|
||||
message: string,
|
||||
duration?: number
|
||||
) => {
|
||||
const ts = Date.now()
|
||||
const toast = {
|
||||
ts,
|
||||
type,
|
||||
message,
|
||||
expires: ts + (duration ?? defaultDuration),
|
||||
}
|
||||
setToasts((prev) => [...prev, toast])
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast }}>
|
||||
{children}
|
||||
<Portal container={() => document.getElementById("toast-root")!}>
|
||||
{toasts.map((toast, i) => (
|
||||
<Transition
|
||||
key={toast.ts}
|
||||
in={!toast.expired}
|
||||
duration={{
|
||||
in: 50,
|
||||
out: 300,
|
||||
}}
|
||||
onTransitionEnd={(state) => {
|
||||
if (state === "exited") {
|
||||
setToasts((prev) => prev.filter((t) => t.ts !== toast.ts))
|
||||
}
|
||||
}}
|
||||
element={(state) => <ToastItem toast={toast} state={state} i={i} />}
|
||||
/>
|
||||
))}
|
||||
</Portal>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ToastItem = memo(
|
||||
({
|
||||
toast,
|
||||
state,
|
||||
i,
|
||||
}: {
|
||||
toast: Toast
|
||||
state: TransitionState
|
||||
i: number
|
||||
}) => {
|
||||
if (state == "exited") return null
|
||||
const translateX = state === "entered" ? 0 : 250
|
||||
const translateY = i * 70
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${translateX}px, ${translateY}px)`,
|
||||
}}
|
||||
className={`toast toast-${toast.type}`}
|
||||
>
|
||||
<span className="toast-message">{toast.message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
@ -3,6 +3,8 @@ import { StickyNoteButton } from "./StickyNoteButton"
|
||||
import { ImageCardButton } from "./ImageCardButton"
|
||||
import { ExportButton } from "./ExportButton"
|
||||
import { TextButton } from "./TextButton"
|
||||
import { ImportButton } from "./ImportButton"
|
||||
import { Divider } from "../Divider"
|
||||
|
||||
export function CardSelector() {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
@ -21,20 +23,11 @@ export function CardSelector() {
|
||||
<Divider />
|
||||
|
||||
<ExportButton />
|
||||
<ImportButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
return (
|
||||
<div style={{
|
||||
margin: '2px 2px',
|
||||
border: "1px solid #9c9c9c",
|
||||
borderRight: 'none',
|
||||
}}></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { ImagesSignal, NotesSigal } from "../../signals"
|
||||
import { createFileAndExport } from "../../utils/createFileAndExport"
|
||||
import { Tooltip } from "./Tooltip"
|
||||
import { defaultClassName } from "./utils"
|
||||
|
||||
export function ExportButton() {
|
||||
@ -11,37 +13,31 @@ export function ExportButton() {
|
||||
...notes.value,
|
||||
...images.value
|
||||
}
|
||||
const date = new Date().toDateString().split(' ').join('_')
|
||||
const name = `Kslab_export_${date}.json`
|
||||
const name = `Kslab_export`
|
||||
const jsonData = JSON.stringify(mergeState)
|
||||
const file = new File([jsonData], name, {
|
||||
type: 'text/json'
|
||||
})
|
||||
const url = URL.createObjectURL(file)
|
||||
const a = document.createElement('a')
|
||||
a.download = name
|
||||
a.href = url
|
||||
a.click()
|
||||
createFileAndExport(name, jsonData, "text/json")
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
onclick={_handleExport}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}
|
||||
>
|
||||
<path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" x2="12" y1="15" y2="3" />
|
||||
</svg>
|
||||
<Tooltip message="Export Json File">
|
||||
<svg
|
||||
onclick={_handleExport}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}
|
||||
>
|
||||
<path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" x2="12" y1="15" y2="3" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,18 @@
|
||||
import { ImagesSignal } from "../../signals"
|
||||
import images from "../../signals/images"
|
||||
import { updateLocalStorage } from "../../utils/localStorage"
|
||||
import { useToast } from "../Toast"
|
||||
import { Tooltip } from "./Tooltip"
|
||||
import { defaultClassName } from "./utils"
|
||||
|
||||
export function ImageCardButton() {
|
||||
const toast = useToast()
|
||||
function _handleClick(mouseEvent: MouseEvent) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = "image/*"
|
||||
input.multiple = false
|
||||
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
@ -26,7 +32,7 @@ export function ImageCardButton() {
|
||||
if (typeof content == 'string') img = content?.split(':')[1]
|
||||
if (!img) return
|
||||
|
||||
ImagesSignal.default.addImage({
|
||||
const imgId = ImagesSignal.default.addImage({
|
||||
type: "image",
|
||||
title: "New Image",
|
||||
contents: content as string,
|
||||
@ -39,7 +45,16 @@ export function ImageCardButton() {
|
||||
h: normalizedH
|
||||
}
|
||||
})
|
||||
updateLocalStorage("images", images.images.value)
|
||||
|
||||
try {
|
||||
updateLocalStorage("images", images.images.value)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof DOMException) {
|
||||
if (e.name !== 'QuotaExceededError') return
|
||||
toast.showToast("error", "Could not add such a girthy image!")
|
||||
ImagesSignal.default.removeImage(imgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
image.src = readerEvent.target?.result as string
|
||||
}
|
||||
@ -48,31 +63,33 @@ export function ImageCardButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
onclick={_handleClick}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}>
|
||||
<rect
|
||||
width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
ry="2" />
|
||||
<circle
|
||||
cx="9"
|
||||
cy="9"
|
||||
r="2" />
|
||||
<path
|
||||
d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
<Tooltip message="Create an Image">
|
||||
<svg
|
||||
onclick={_handleClick}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}>
|
||||
<rect
|
||||
width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
ry="2" />
|
||||
<circle
|
||||
cx="9"
|
||||
cy="9"
|
||||
r="2" />
|
||||
<path
|
||||
d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
69
src/components/cardSelector/ImportButton.tsx
Normal file
69
src/components/cardSelector/ImportButton.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import images from "../../signals/images"
|
||||
import notes from "../../signals/notes"
|
||||
import { Card } from "../../types"
|
||||
import { convertBase64ToJson } from "../../utils/convertBase64ToJson"
|
||||
import { updateLocalStorage } from "../../utils/localStorage"
|
||||
import { Tooltip } from "./Tooltip"
|
||||
import { defaultClassName } from "./utils"
|
||||
|
||||
export function ImportButton() {
|
||||
|
||||
function _handleImport() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = ".json"
|
||||
input.multiple = false
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = function(readerEvent) {
|
||||
let content = readerEvent.target?.result;
|
||||
// get only the base64 parts and not any identifiers
|
||||
content = (content as string).split(',')[1]
|
||||
const data: Record<string, Card<'note'> | Card<'image'>> = convertBase64ToJson(content)
|
||||
for (let key in data) {
|
||||
const item = data[key]
|
||||
if (item.type == 'image') {
|
||||
const { id, ...rest } = item
|
||||
images.addImage(rest)
|
||||
}
|
||||
|
||||
if (item.type == 'note') {
|
||||
const { id, ...rest } = item
|
||||
notes.addNote(rest)
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalStorage('notes', notes.notes.value)
|
||||
updateLocalStorage('images', images.images.value)
|
||||
notes.notes.notify()
|
||||
images.images.notify()
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip message="Import Json File">
|
||||
<svg
|
||||
onclick={_handleImport}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={"rotate-[180deg] " + defaultClassName}
|
||||
>
|
||||
<path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" x2="12" y1="15" y2="3" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { NotesSigal } from "../../signals"
|
||||
import notes from "../../signals/notes"
|
||||
import { updateLocalStorage } from "../../utils/localStorage"
|
||||
import { Tooltip } from "./Tooltip"
|
||||
import { defaultClassName } from "./utils"
|
||||
|
||||
export function StickyNoteButton() {
|
||||
@ -22,23 +23,25 @@ export function StickyNoteButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
onclick={_handleClick}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}>
|
||||
<path
|
||||
d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
||||
<path
|
||||
d="M15 3v4a2 2 0 0 0 2 2h4" />
|
||||
</svg>
|
||||
<Tooltip message="Create a Sticky Note">
|
||||
<svg
|
||||
onclick={_handleClick}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}>
|
||||
<path
|
||||
d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
||||
<path
|
||||
d="M15 3v4a2 2 0 0 0 2 2h4" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,49 @@
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { TextSignal } from "../../signals";
|
||||
import texts from "../../signals/texts";
|
||||
import { updateLocalStorage } from "../../utils/localStorage";
|
||||
import { defaultClassName } from "./utils";
|
||||
|
||||
export function TextButton() {
|
||||
|
||||
function _handleClick(e: MouseEvent) {
|
||||
TextSignal.default.addText({
|
||||
type: "text",
|
||||
title: "New Note",
|
||||
contents: "todo: fill me",
|
||||
position: {
|
||||
x: e.pageX - 100,
|
||||
y: e.pageY + (window.innerHeight / 2) - 100
|
||||
},
|
||||
dimensions: {
|
||||
w: 200,
|
||||
h: 100
|
||||
}
|
||||
})
|
||||
updateLocalStorage("text", texts.texts.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip message="Create a Text Node">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={defaultClassName}
|
||||
>
|
||||
<path d="M4 20h16" />
|
||||
<path d="m6 16 6-12 6 12" />
|
||||
<path d="M8 12h8" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
<svg
|
||||
onclick={_handleClick}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
|
8
src/components/cardSelector/Tooltip.tsx
Normal file
8
src/components/cardSelector/Tooltip.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export function Tooltip({ children, message }: { children: JSX.Element, message: string }) {
|
||||
return (
|
||||
<div title={message}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
26
src/components/icons/ExportIcon.tsx
Normal file
26
src/components/icons/ExportIcon.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
namespace ExportIcon {
|
||||
export interface Props {
|
||||
className?: string
|
||||
}
|
||||
}
|
||||
export function ExportIcon({ className }: ExportIcon.Props) {
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className={className ?? ""}
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" x2="12" y1="3" y2="15" />
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -18,6 +18,7 @@ function addImage(data: Omit<ImageCardType, "id">) {
|
||||
images.value[newCard.id] = newCard
|
||||
images.notify()
|
||||
focusedItem.value = newCard.id
|
||||
return newCard.id
|
||||
}
|
||||
|
||||
function removeImage(id: ImageCardType["id"]) {
|
||||
|
@ -6,3 +6,4 @@ export const canvasDimentsion = signal({ width: 3000, height: 3000 })
|
||||
|
||||
export * as NotesSigal from "./notes"
|
||||
export * as ImagesSignal from "./images"
|
||||
export * as TextSignal from "./texts"
|
||||
|
46
src/signals/texts.ts
Normal file
46
src/signals/texts.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { signal } from "kaioken"
|
||||
import { Card } from "../types"
|
||||
import { focusedItem } from "."
|
||||
|
||||
export type TextCardType = Card<"text">
|
||||
|
||||
const texts = signal<Record<TextCardType["id"], TextCardType>>({})
|
||||
|
||||
function loadLocalStorage() {
|
||||
texts.value = JSON.parse(localStorage.getItem("texts") ?? "{}")
|
||||
}
|
||||
|
||||
function addText(data: Omit<TextCardType, "id">) {
|
||||
const newCard = {
|
||||
...data,
|
||||
id: crypto.randomUUID(),
|
||||
}
|
||||
texts.value[newCard.id] = newCard
|
||||
texts.notify()
|
||||
focusedItem.value = newCard.id
|
||||
}
|
||||
|
||||
function removeText(id: TextCardType["id"]) {
|
||||
delete texts.value[id]
|
||||
texts.notify()
|
||||
}
|
||||
function updateTextProperty<K extends keyof TextCardType>(
|
||||
id: TextCardType["id"],
|
||||
property: K,
|
||||
data: TextCardType[K]
|
||||
) {
|
||||
const newData = {
|
||||
...texts.value[id],
|
||||
[property]: data,
|
||||
}
|
||||
texts.value[id] = newData
|
||||
texts.notify()
|
||||
}
|
||||
|
||||
export default {
|
||||
texts,
|
||||
addText,
|
||||
removeText,
|
||||
updateTextProperty,
|
||||
loadLocalStorage,
|
||||
}
|
@ -2,20 +2,26 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("./assets/fonts/Virgil.woff2") format("woff2");
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-synthesis: none;
|
||||
@ -29,3 +35,29 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toast {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
@apply fixed right-4 top-4 w-52 px-4;
|
||||
@apply transition-all duration-300 rounded z-50;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
.toast.toast-info {
|
||||
@apply bg-info;
|
||||
}
|
||||
|
||||
.toast.toast-success {
|
||||
@apply bg-success;
|
||||
}
|
||||
|
||||
.toast.toast-warning {
|
||||
@apply bg-warning;
|
||||
}
|
||||
|
||||
.toast.toast-error {
|
||||
@apply bg-error;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export type CardTypes = "note" | "image"
|
||||
export type CardTypes = "note" | "image" | "text"
|
||||
export type positionCoords = { x: number; y: number }
|
||||
export type dimensionCoords = { w: number; h: number }
|
||||
|
||||
|
19
src/utils/convertBase64ToJson.ts
Normal file
19
src/utils/convertBase64ToJson.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export function convertBase64ToJson(
|
||||
data: string | ArrayBuffer | null | undefined
|
||||
) {
|
||||
try {
|
||||
if (!data) throw new Error("no data to decode")
|
||||
if (data instanceof ArrayBuffer)
|
||||
throw new Error("cannot decode array buffer yet")
|
||||
// Decode Base64 to string
|
||||
const jsonString = atob(data)
|
||||
|
||||
// Parse string to JSON
|
||||
const jsonObject = JSON.parse(jsonString)
|
||||
|
||||
// Display the JSON object
|
||||
return jsonObject
|
||||
} catch (error) {
|
||||
console.error("Error decoding Base64:", error)
|
||||
}
|
||||
}
|
23
src/utils/createFileAndExport.ts
Normal file
23
src/utils/createFileAndExport.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createFileCompliantDateString } from "./createFileCompliantDateString"
|
||||
import { createFileFromData } from "./createFileFromData"
|
||||
|
||||
export type acceptableFileTypes = "text/json" | "text/markdown"
|
||||
export function createFileAndExport(
|
||||
name: string,
|
||||
data: string,
|
||||
type: acceptableFileTypes
|
||||
) {
|
||||
const date = createFileCompliantDateString()
|
||||
const fileName = `${name}_${date}.${_getFileType(type)}`
|
||||
const file = createFileFromData(data, fileName, "text/json")
|
||||
const url = URL.createObjectURL(file)
|
||||
const a = document.createElement("a")
|
||||
a.download = fileName
|
||||
a.href = url
|
||||
a.click()
|
||||
}
|
||||
|
||||
function _getFileType(type: acceptableFileTypes): "json" | "md" {
|
||||
if (type === "text/markdown") return "md"
|
||||
return "json"
|
||||
}
|
3
src/utils/createFileCompliantDateString.ts
Normal file
3
src/utils/createFileCompliantDateString.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function createFileCompliantDateString() {
|
||||
return new Date().toDateString().split(" ").join("_")
|
||||
}
|
9
src/utils/createFileFromData.ts
Normal file
9
src/utils/createFileFromData.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function createFileFromData(
|
||||
data: string,
|
||||
name: string,
|
||||
type: BlobPropertyBag["type"]
|
||||
) {
|
||||
return new File(Array.from(data), name, {
|
||||
type: type,
|
||||
})
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export function isTheme(value: "light" | "dark") {
|
||||
return window.matchMedia(`(prefers-color-scheme: ${value})`).matches
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { CardTypes } from "../types"
|
||||
|
||||
export function updateLocalStorage(
|
||||
location: "notes" | "images",
|
||||
location: CardTypes,
|
||||
collection: unknown[] | Record<string, unknown>
|
||||
) {
|
||||
localStorage.setItem(location, JSON.stringify(collection))
|
||||
|
20
src/utils/useThemeDetector.ts
Normal file
20
src/utils/useThemeDetector.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from "kaioken"
|
||||
|
||||
export function useThemeDetector() {
|
||||
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(getCurrentTheme())
|
||||
function getCurrentTheme() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
}
|
||||
|
||||
const mqListener = (e: MediaQueryListEvent) => {
|
||||
setIsDarkTheme(e.matches)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
darkThemeMq.addListener(mqListener)
|
||||
return () => darkThemeMq.removeListener(mqListener)
|
||||
}, [])
|
||||
|
||||
return isDarkTheme
|
||||
}
|
@ -4,10 +4,12 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "rgb(64 37 121)",
|
||||
"primary-light": "rgb(195 177 232)",
|
||||
info: "#4f46e5",
|
||||
success: "#267d46",
|
||||
warning: "#a46319",
|
||||
error: "#963030",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user