Compare commits

...

29 Commits

Author SHA1 Message Date
Triston Armstrong
3d61f28aa0
working on some text logic playing with ideas 2024-10-10 04:47:04 -04:00
Triston Armstrong
c364996229
(feat): make text appear on screen and minimap 2024-10-10 04:47:03 -04:00
a7522db83d Merge pull request 'feat(favicon): add favicon to page' (#8) from feat/favicon into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/8
2024-10-10 08:11:45 +00:00
44e7154084
feat(favicon): add favicon to page 2024-10-10 04:10:24 -04:00
fd1d55de5d Merge pull request 'feat(storage): remember where the user was last on the screen' (#7) from feat/location-memory into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/7
2024-10-10 07:40:57 +00:00
ae17593cfb
feat(storage): remember where the user was last on the screen
When a user refreshes the page, it will now scroll the users position to the position they were when
last on this page
2024-10-10 03:39:17 -04:00
1071743290
fix(images): adds a toaster and handles failed image inserts
The image insert will throw a dom exception if the image size exceeds the alotted storage provided
by local storage. This just lets the user know somethng fufuckedd up and atleast removeds the image
after inserting (inknow, dont tell me) This iwill be solved in the future with IDB
2024-10-10 03:35:35 -04:00
3fd6aef52c
style: change icon and code styles 2024-10-09 13:59:33 -04:00
261eb8348e Merge pull request 'feat(notes): add support for exporting individual notes' (#6) from feat/note-export into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/6
2024-10-09 17:10:00 +00:00
77624aae8c
feat(notes): add support for exporting individual notes
This adds an export icon to the notes cards so a user can export an individual note for sharing or
saaving for later
2024-10-09 12:55:58 -04:00
f659a38026 Merge pull request 'feat/md' (#5) from feat/md into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/5
2024-10-09 04:32:57 +00:00
4ea03e68bf
update gray items border color 2024-10-09 00:31:52 -04:00
412ae1c740
refactor a bit for readability 2024-10-09 00:22:53 -04:00
640e5af6fe
fix styling of markdown text 2024-10-08 23:57:24 -04:00
80e5f03877
get markdown working with existing notes cards 2024-10-08 01:34:29 -04:00
6d4d825d5f
chore(package): update bun lock file 2024-10-07 13:04:34 -04:00
f40fb009f3
chore: adds commitizen
We needed better, more well formatted commit messages, so i installed commitizen so hopefully this
can be adhered to
2024-10-07 12:54:08 -04:00
4e86455c5d
bug/fix os theme switch detection 2024-10-07 12:32:27 -04:00
3bc92ecb7d
allow all image types 2024-10-07 03:16:59 -04:00
4507e1fe8e
update browserslist 2024-10-07 03:09:47 -04:00
22918a8d72
chore/limit what file types can be added 2024-10-07 03:08:56 -04:00
f67649b6b7
update app name 2024-10-07 02:54:35 -04:00
0ef4517993 Merge pull request 'add logo for app' (#4) from feat/logo into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/4
2024-10-07 06:48:26 +00:00
81b7531f89
add logo for app 2024-10-07 02:47:19 -04:00
22d5c3ed4e Merge pull request 'add tooltips to creation buttons' (#3) from feat/tooltips into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/3
2024-10-07 05:33:55 +00:00
Triston Armstrong
6bbed4100a add tooltips to creation buttons 2024-10-07 01:33:25 -04:00
a09724b38a Merge pull request 'feat/import' (#2) from feat/import into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/2
2024-10-07 04:55:19 +00:00
Triston Armstrong
2ed58cbee6 limit types of files to import 2024-10-07 00:54:50 -04:00
Triston Armstrong
4dcea1a42a add ability to export and import to other browsers 2024-10-07 00:52:12 -04:00
40 changed files with 965 additions and 180 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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>

View File

@ -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
View File

@ -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"

View File

@ -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"

View File

@ -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",

View File

@ -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>
)
}

Binary file not shown.

7
src/assets/logo.svg Normal file
View 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

View File

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

View File

@ -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
View 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>
</>
)
}

View 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>
)
}

View 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;
}

View File

@ -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`,

View File

@ -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)
}
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={{
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`,
}}
>
<div className="flex-1 flex flex-col gap-1">
<div className="px-2 flex justify-between items-center cursor-move" onmousedown={_handleMouseDown}>
<div style={{
}
const saveIndicatorStyle = {
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
}}
}
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"
>
<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={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-[#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
View 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
View 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>
)
}
)

View File

@ -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>
)
}

View File

@ -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,20 +13,13 @@ 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 (
<Tooltip message="Export Json File">
<svg
onclick={_handleExport}
xmlns="http://www.w3.org/2000/svg"
@ -43,5 +38,6 @@ export function ExportButton() {
<polyline points="7 10 12 15 17 10" />
<line x1="12" x2="12" y1="15" y2="3" />
</svg>
</Tooltip>
)
}

View File

@ -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
}
})
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,6 +63,7 @@ export function ImageCardButton() {
}
return (
<Tooltip message="Create an Image">
<svg
onclick={_handleClick}
xmlns="http://www.w3.org/2000/svg"
@ -74,5 +90,6 @@ export function ImageCardButton() {
<path
d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
</Tooltip>
)
}

View 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>
)
}

View File

@ -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,6 +23,7 @@ export function StickyNoteButton() {
}
return (
<Tooltip message="Create a Sticky Note">
<svg
onclick={_handleClick}
xmlns="http://www.w3.org/2000/svg"
@ -39,6 +41,7 @@ export function StickyNoteButton() {
<path
d="M15 3v4a2 2 0 0 0 2 2h4" />
</svg>
</Tooltip>
)
}

View File

@ -1,8 +1,30 @@
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"
@ -19,5 +41,23 @@ export function TextButton() {
<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"
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>
)
}

View File

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

View 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>
)
}

View File

@ -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"]) {

View File

@ -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
View 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,
}

View File

@ -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;
}

View File

@ -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 }

View 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)
}
}

View 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"
}

View File

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

View File

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

View File

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

View File

@ -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))

View 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
}

View File

@ -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: [],
};
}