Compare commits

..

2 Commits

Author SHA1 Message Date
Triston Armstrong
5b0b8c2224 working on some text logic playing with ideas 2024-10-07 00:28:54 -04:00
Triston Armstrong
95924ba49f (feat): make text appear on screen and minimap 2024-10-06 23:36:11 -04:00
35 changed files with 178 additions and 744 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -4,18 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<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" />
<title>KlectrTemplate</title>
</head>
<body>
<div id="toast-root"></div>
<div id="root"></div>
<div id="root" />
<script type="module" src="/src/main.ts" defer></script>
</body>
</html>

View File

@ -1,35 +1,25 @@
{
"name": "k-slab",
"private": false,
"name": "ktemplate",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"commit": "cz"
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^1",
"caniuse-lite": "^1.0.30001667",
"kaioken": "^0.32.1",
"tiny-markdown-editor": "^0.1.26"
"kaioken": "^0.32.1"
},
"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
View File

@ -2,6 +2,16 @@
# 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"
@ -1396,16 +1406,6 @@ 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"

View File

@ -1,7 +1,7 @@
[package]
name = "k-slab"
name = "KTemplate"
version = "1.0.0"
description = "A Canvas for your notes"
description = "A Kaioken Tauri App Template"
authors = ["tristonarmstrong"]
edition = "2021"

View File

@ -7,7 +7,7 @@
"withGlobalTauri": true
},
"package": {
"productName": "Kslab",
"productName": "KlectrTemplate",
"version": "0.0.0"
},
"tauri": {
@ -20,7 +20,7 @@
},
"windows": [
{
"title": "Kslab",
"title": "KlectrTemplate",
"width": 500,
"height": 500
}
@ -31,7 +31,7 @@
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.k-slab.dev",
"identifier": "com.klectr-template.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",

View File

@ -1,13 +1,5 @@
import InfiniteCanvas from "./components/InfinateCanvas"
import { ToastContextProvider } from "./components/Toast"
import { useThemeDetector } from "./utils/useThemeDetector"
export function App() {
useThemeDetector()
return (
<ToastContextProvider>
<InfiniteCanvas />
</ToastContextProvider>
)
return <InfiniteCanvas />
}

Binary file not shown.

View File

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,7 +0,0 @@
export function Divider() {
return (
<div className="border-l-[1px] dark:border-l-[#5c5c5c] border-l-[#9c9c9c]"></div>
)
}

View File

@ -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 { useThemeDetector } from "../utils/useThemeDetector"
import { isTheme } from "../utils/isTheme"
namespace ImageCard {
export interface ImageCardProps {
@ -121,7 +121,6 @@ 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}
@ -130,7 +129,7 @@ function ExpandIcon({ cb }: {
height="24"
viewBox="0 0 24 24"
fill="none"
stroke={isDarkTheme ? "#777" : "#999"}
stroke={isTheme('dark') ? "#777" : "#999"}
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"

View File

@ -6,50 +6,39 @@ 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(() => {
const initPos = getInitialPosition(canvasDimentsion)
window.scrollTo(initPos)
window.scrollTo({
left: (canvasDimentsion.value.width / 2) - (window.innerWidth / 2),
top: (canvasDimentsion.value.height / 2) - (window.innerHeight / 2)
})
const _updateDimensions = () => {
const updateDimensions = () => {
canvasDimentsion.value = {
width: Math.max(canvasDimentsion.value.width, window.innerWidth),
height: Math.max(canvasDimentsion.value.height, window.innerHeight),
}
}
function _updatePosition() {
localStorage.setItem("pos", JSON.stringify({
left: window.scrollX,
top: window.scrollY
}))
}
_updateDimensions()
window.addEventListener("resize", _updateDimensions)
window.addEventListener("scrollend", _updatePosition)
updateDimensions()
window.addEventListener("resize", updateDimensions)
notes.loadLocalStorage()
images.loadLocalStorage()
texts.loadLocalStorage()
return () => {
window.removeEventListener("resize", _updateDimensions)
window.removeEventListener("scrollend", _updatePosition)
window.removeEventListener("resize", updateDimensions)
}
}, [])
return (
<>
<Logo />
<CardSelector />
<MiniMap />
@ -63,7 +52,7 @@ export default function InfiniteCanvas() {
width: `${canvasDimentsion.value.width}px`,
height: `${canvasDimentsion.value.width}px`,
backgroundSize: "30px 30px",
backgroundImage: `radial-gradient(circle, rgba(${isDarkTheme ? '255, 255, 255, 0.2' : '0, 0, 0, 0.2'}) 1px, transparent 1px)`,
backgroundImage: `radial-gradient(circle, rgba(${isTheme('dark') ? '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]
@ -91,32 +80,3 @@ export default function InfiniteCanvas() {
</>
)
}
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
}

View File

@ -1,9 +0,0 @@
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>
</>
)
}

View File

@ -1,30 +0,0 @@
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>
)
}

View File

@ -1,47 +0,0 @@
.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;
}

View File

@ -22,7 +22,6 @@ 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
@ -33,6 +32,8 @@ export function MiniMap() {
return () => window.removeEventListener('scroll', _handleScroll)
}, [])
return (
<div
className="dark:bg-[#ffffff11] bg-[#0001] fixed rounded"
@ -48,31 +49,22 @@ 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: newLeft,
top: newTop,
left: image.position.x - ((viewportWidth / 2) - (image.dimensions.w / 2)),
top: image.position.y - ((viewportHeight / 2) - (image.dimensions.h / 2)),
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: `${newWidth}px`,
height: `${newHeight}px`,
top: `${newTop}px`,
left: `${newLeft}px`,
zIndex: `${newZIndex}`
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}`
}}
></div>
)
@ -83,30 +75,22 @@ 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: newLeft,
top: newTop,
left: note.position.x - ((viewportWidth / 2) - (note.dimensions.w / 2)),
top: note.position.y - ((viewportHeight / 2) - (note.dimensions.h / 2)),
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"}
<div className={"absolute dark:bg-gray-500 bg-gray-300 hover:bg-blue-500 cursor-pointer border dark:border-[#222] border-gray-500 rounded"}
onclick={_handleItemClick}
style={{
width: `${newWidth}px`,
height: `${newHeight}px`,
top: `${newTop}px`,
left: `${newLeft}px`,
zIndex: `${newZIndex}`
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`,
zIndex: `${LayerEnum.MINIMAP + 1}`
}}
></div>
)
@ -142,7 +126,7 @@ export function MiniMap() {
})}
<div
className={'pointer-events-none absolute bg-blue-200 bg-opacity-10 border dark:border-blue-800 border-blue-500 bg-blue-500 rounded'}
className={'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`,

View File

@ -3,12 +3,7 @@ import { NotesSigal, focusedItem } from "../signals"
import { useDebounce } from "../utils/useDebounce"
import notes, { NoteCardType } from "../signals/notes"
import { LayerEnum } from "../utils/enums"
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"
import { isTheme } from "../utils/isTheme"
namespace NoteCard {
export interface NoteCardProps {
@ -32,7 +27,6 @@ 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)
}
@ -92,71 +86,45 @@ 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={_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"
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`,
}}
>
<div className="overflow-hidden flex-1 flex flex-col gap-1">
{/* Header Bar */}
<div className="flex-1 flex flex-col gap-1">
<div className="px-2 flex justify-between items-center cursor-move" onmousedown={_handleMouseDown}>
<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 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>
<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 >
@ -166,8 +134,6 @@ 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}
@ -176,7 +142,7 @@ function ExpandIcon({ cb }: {
height="24"
viewBox="0 0 24 24"
fill="none"
stroke={isDarkTheme ? "#777" : "#999"}
stroke={isTheme('dark') ? "#777" : "#999"}
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
@ -188,4 +154,3 @@ function ExpandIcon({ cb }: {
</svg>
)
}

View File

@ -1,112 +0,0 @@
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>
)
}
)

View File

@ -3,8 +3,6 @@ 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)
@ -23,11 +21,20 @@ export function CardSelector() {
<Divider />
<ExportButton />
<ImportButton />
</div>
)
}
function Divider() {
return (
<div style={{
margin: '2px 2px',
border: "1px solid #9c9c9c",
borderRight: 'none',
}}></div>
)
}

View File

@ -1,6 +1,4 @@
import { ImagesSignal, NotesSigal } from "../../signals"
import { createFileAndExport } from "../../utils/createFileAndExport"
import { Tooltip } from "./Tooltip"
import { defaultClassName } from "./utils"
export function ExportButton() {
@ -13,31 +11,37 @@ export function ExportButton() {
...notes.value,
...images.value
}
const name = `Kslab_export`
const date = new Date().toDateString().split(' ').join('_')
const name = `Kslab_export_${date}.json`
const jsonData = JSON.stringify(mergeState)
createFileAndExport(name, jsonData, "text/json")
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()
}
return (
<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>
<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>
)
}

View File

@ -1,18 +1,12 @@
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()
@ -32,7 +26,7 @@ export function ImageCardButton() {
if (typeof content == 'string') img = content?.split(':')[1]
if (!img) return
const imgId = ImagesSignal.default.addImage({
ImagesSignal.default.addImage({
type: "image",
title: "New Image",
contents: content as string,
@ -45,16 +39,7 @@ export function ImageCardButton() {
h: normalizedH
}
})
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)
}
}
updateLocalStorage("images", images.images.value)
}
image.src = readerEvent.target?.result as string
}
@ -63,33 +48,31 @@ export function ImageCardButton() {
}
return (
<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>
<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>
)
}

View File

@ -1,69 +0,0 @@
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>
)
}

View File

@ -1,7 +1,6 @@
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() {
@ -23,25 +22,23 @@ export function StickyNoteButton() {
}
return (
<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>
<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>
)
}

View File

@ -1,4 +1,3 @@
import { Tooltip } from "./Tooltip";
import { TextSignal } from "../../signals";
import texts from "../../signals/texts";
import { updateLocalStorage } from "../../utils/localStorage";
@ -24,24 +23,6 @@ export function TextButton() {
}
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"

View File

@ -1,8 +0,0 @@
export function Tooltip({ children, message }: { children: JSX.Element, message: string }) {
return (
<div title={message}
>
{children}
</div>
)
}

View File

@ -1,26 +0,0 @@
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>
)
}

View File

@ -18,7 +18,6 @@ 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"]) {

View File

@ -2,24 +2,18 @@
@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;
}
html {
overflow: scroll;
*::-webkit-scrollbar {
display: none;
}
html,
body {
overscroll-behavior-x: none;
html {
overflow: scroll;
}
:root {
@ -35,29 +29,3 @@ 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;
}

View File

@ -1,19 +0,0 @@
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)
}
}

View File

@ -1,23 +0,0 @@
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"
}

View File

@ -1,3 +0,0 @@
export function createFileCompliantDateString() {
return new Date().toDateString().split(" ").join("_")
}

View File

@ -1,9 +0,0 @@
export function createFileFromData(
data: string,
name: string,
type: BlobPropertyBag["type"]
) {
return new File(Array.from(data), name, {
type: type,
})
}

3
src/utils/isTheme.ts Normal file
View File

@ -0,0 +1,3 @@
export function isTheme(value: "light" | "dark") {
return window.matchMedia(`(prefers-color-scheme: ${value})`).matches
}

View File

@ -1,20 +0,0 @@
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
}

View File

@ -4,12 +4,10 @@ export default {
theme: {
extend: {
colors: {
info: "#4f46e5",
success: "#267d46",
warning: "#a46319",
error: "#963030",
primary: "rgb(64 37 121)",
"primary-light": "rgb(195 177 232)",
},
},
},
plugins: [],
}
};