Compare commits

...

19 Commits

Author SHA1 Message Date
e40d405840 update timer type 2024-11-08 01:13:02 -05:00
f9d0315bd7
build: add vite plugin checker
this throws ts errors if any found in project so were not missing anything
2024-10-25 11:19:41 +07:00
fee06527a8
fix: fix linter and type errors 2024-10-25 11:18:42 +07:00
f436b11102
update readme with some boilerplate content 2024-10-23 13:47:59 +07:00
1f410dbd56
fix notecard help paper colors 2024-10-23 13:47:50 +07:00
26ee1d8921
fix help icon colors 2024-10-23 13:47:36 +07:00
441269d435
make it so underscore ignores unused vars 2024-10-23 13:27:28 +07:00
a506da11ad
fix a bunch of lint errors 2024-10-23 12:40:20 +07:00
958124cd9e
begin refactoring card types 2024-10-23 11:48:47 +07:00
a7a84a24e1
setup eslint integrations 2024-10-23 11:47:57 +07:00
ad8ed2f596 Merge pull request 'feat/hotkeys' (#11) from feat/hotkeys into main
Reviewed-on: #11
2024-10-22 18:31:51 +00:00
32b05d76f0 Merge pull request 'fix(scroll): fix issue with scrolll positino not persisting on reload' (#12) from bug/safari-scroll into main
Reviewed-on: #12
2024-10-22 18:31:40 +00:00
f505d002f7
fix(scroll): fix issue with scrolll positino not persisting on reload 2024-10-22 14:30:21 -04:00
2398d10c9f
feat(notes): fix help icon and save icon positioning 2024-10-22 14:07:48 -04:00
f7b392f909
feat(notes): add blue border to help paper 2024-10-21 05:32:18 -04:00
ff7043a35d
feat(notes): add hint to notes so we can see available hotkeys 2024-10-21 05:14:46 -04:00
a662b5abc0
feat(notes): make basic hotkeys work
works on delete, backspace, and export
2024-10-12 15:51:08 -04:00
be10f376f4
fix(image): delete image wasnt updating storage 2024-10-12 13:51:34 -04:00
98fc00bac1 Merge pull request 'feat/context-menu' (#10) from feat/context-menu into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/10
2024-10-12 17:32:31 +00:00
27 changed files with 463 additions and 185 deletions

View File

@ -1,7 +1,86 @@
# Tauri + Vanilla TS ### **Kslab**
This template should help get you started developing with [Tauri](https://tauri.app) & [Kaioken](https://kaioken.dev). A visual organization tool for notes, text, shapes, and images.
## Recommended IDE Setup ### **Table of Contents**
* [Technologies]()
* [Development Setup]()
* [Installation]()
* [Desktop Application]()
* [Web Application]()
* [Usage]()
* [Desktop Application]()
* [Web Application]()
* [Contributing]()
* [License]()
### **Screenshots**
![screenshot](./src/assets/repo/screenshot.png)
### **Technologies**
This project utilizes the following open-source libraries:
* **Kaioken:** A lightweight UI rendering library ([Kaioken](https://kaioken.dev))
* **Tauri:** A framework for building desktop and mobile applications with web technologies ([https://v2.tauri.app/](https://v2.tauri.app/))
* **TypeScript:** A typed superset of JavaScript that adds optional static typing
* **ESLint:** A linter for enforcing code style and catching potential errors
### **Development Setup**
1. **Clone the repository:**
```bash
git clone https://git.klectr.dev/Klectr/kslab.git
```
2. **Install dependencies:**
This project uses [Bun](https://bun.sh) as its package manager.
```bash
bun install
```
### **Installation**
#### **Desktop Application**
[Instructions on how to download, install, and run the desktop application. Mention any dependencies or prerequisites.]
**Note:** These instructions might involve installing Tauri and any additional dependencies for the desktop application.
#### **Web Application**
[Instructions on how to access and use the web application. Specify if it's a standalone web app or runs within Tauri.]
**Note:** These instructions might involve running the web application directly in a browser if it's standalone.
### **Usage**
#### **Desktop Application**
* **Add notes:** Click the "+" button to create a new note.
* **Edit notes:** Double-click a note to edit its content.
* **Move notes:** Drag and drop notes to reposition them on the canvas.
* **Add shapes and images:** Use the toolbar to add various shapes and images.
* **Customize appearance:** Change the color, size, and other properties of elements.
#### **Web Application**
[Instructions for using the web application, similar to the desktop application]
### **Contributing**
[Guidelines for contributors, including how to report bugs or submit pull requests. Mention how contributions to both Kaioken and Tauri integrations are handled. Also, emphasize the importance of following TypeScript and ESLint conventions.]
### **License**
[Specify the license under which the project is released (e.g., MIT, Apache License 2.0)]
**Additional sections you might consider:**
* **Roadmap** - Future plans for Kslab, such as implementing an infinite canvas or adding more features.
**Remember to replace the placeholder text with specific details about your project.**
**Let me know if you'd like help filling in any specific sections or if you have any further questions about Kaioken, Tauri, TypeScript, or ESLint.**
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

BIN
bun.lockb

Binary file not shown.

40
eslint.config.js Normal file
View File

@ -0,0 +1,40 @@
import globals from "globals"
import pluginJs from "@eslint/js"
import tseslint from "typescript-eslint"
import pluginReact from "eslint-plugin-react"
export default [
{ files: ["./src/**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
{ plugins: { react: pluginReact } },
{
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
globals: globals.browser,
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
rules: {
"react/react-in-jsx-scope": "off",
"react/no-unknown-property": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
},
]

View File

@ -18,14 +18,22 @@
"tiny-markdown-editor": "^0.1.26" "tiny-markdown-editor": "^0.1.26"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.13.0",
"@tauri-apps/cli": "^1", "@tauri-apps/cli": "^1",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"commitizen": "^4.3.1", "commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.11.0",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-kaioken": "^0.13.1" "vite-plugin-kaioken": "^0.13.1"
}, },
"config": { "config": {

View File

@ -1,14 +1,12 @@
import { useClickOutside, useKeyStroke, useMouse } from "@kaioken-core/hooks"; import { useClickOutside, useMouse } from "@kaioken-core/hooks";
import { Portal, signal, useEffect, useRef } from "kaioken"; import { Portal, signal, useEffect, useRef } from "kaioken";
namespace ContextMenuPortal { interface ContextMenuPortalProps {
export interface Props { children: JSX.Children
children: JSX.Children open: boolean
open: boolean closeAction: (() => void) | null | undefined
closeAction: (() => void) | null | undefined
}
} }
export function ContextMenuPortal({ children, open, closeAction }: ContextMenuPortal.Props) { export function ContextMenuPortal({ children, open, closeAction }: ContextMenuPortalProps) {
const { mouse } = useMouse() const { mouse } = useMouse()
const pos = signal({ x: 0, y: 0 }) const pos = signal({ x: 0, y: 0 })
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)

View File

@ -6,15 +6,14 @@ import images, { ImageCardType } from "../signals/images"
import { updateLocalStorage } from "../utils/localStorage" import { updateLocalStorage } from "../utils/localStorage"
import { useThemeDetector } from "../utils/useThemeDetector" import { useThemeDetector } from "../utils/useThemeDetector"
import { ContextMenuPortal } from "./ContextMenuPortal" import { ContextMenuPortal } from "./ContextMenuPortal"
import { CardTypes } from "../types"
namespace ImageCard { interface ImageCardProps {
export interface ImageCardProps { key: ImageCardType['id']
key: ImageCardType['id'] data: ImageCardType
data: ImageCardType
}
} }
export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps) { export function ImageCard({ key: itemKey, data: item }: ImageCardProps) {
const { debounce } = useDebounce() const { debounce } = useDebounce()
const pressed = signal(false) const pressed = signal(false)
const newX = useRef(0) const newX = useRef(0)
@ -27,7 +26,7 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps
function debounceLSUpdate(time?: number) { function debounceLSUpdate(time?: number) {
debounce(() => { debounce(() => {
updateLocalStorage("images", images.images.value) updateLocalStorage(CardTypes.IMAGES, images.images).notify()
}, time) }, time)
} }
@ -90,9 +89,10 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps
window.removeEventListener('mouseup', _handleResizeMouseUp) window.removeEventListener('mouseup', _handleResizeMouseUp)
} }
function _handleClose(_e: Event) { function _handleClose() {
ImagesSignal.default.removeImage(item.id) ImagesSignal.default.removeImage(item.id)
ImagesSignal.default.images.notify() ImagesSignal.default.images.notify()
debounceLSUpdate()
} }
function _handleMouseClick(e: MouseEvent) { function _handleMouseClick(e: MouseEvent) {
@ -160,7 +160,7 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps
function ExpandIcon({ cb }: { function ExpandIcon({ cb }: {
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined
}) { }) {
const isDarkTheme = useThemeDetector() const isDarkTheme = useThemeDetector()
return ( return (

View File

@ -10,10 +10,12 @@ import { Logo } from "./Logo"
import { useThemeDetector } from "../utils/useThemeDetector" import { useThemeDetector } from "../utils/useThemeDetector"
import { TextItem } from "./TextItem" import { TextItem } from "./TextItem"
import texts from "../signals/texts" import texts from "../signals/texts"
import { useDebounce } from "../utils/useDebounce"
export default function InfiniteCanvas() { export default function InfiniteCanvas() {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const isDarkTheme = useThemeDetector() const isDarkTheme = useThemeDetector()
const { debounce } = useDebounce()
useEffect(() => { useEffect(() => {
const initPos = getInitialPosition(canvasDimentsion) const initPos = getInitialPosition(canvasDimentsion)
@ -27,22 +29,24 @@ export default function InfiniteCanvas() {
} }
function _updatePosition() { function _updatePosition() {
localStorage.setItem("pos", JSON.stringify({ debounce(() => {
left: window.scrollX, localStorage.setItem("pos", JSON.stringify({
top: window.scrollY left: window.scrollX,
})) top: window.scrollY
}))
})
} }
_updateDimensions() _updateDimensions()
window.addEventListener("resize", _updateDimensions) window.addEventListener("resize", _updateDimensions)
window.addEventListener("scrollend", _updatePosition) window.addEventListener("scroll", _updatePosition)
notes.loadLocalStorage() notes.loadLocalStorage()
images.loadLocalStorage() images.loadLocalStorage()
texts.loadLocalStorage() texts.loadLocalStorage()
return () => { return () => {
window.removeEventListener("resize", _updateDimensions) window.removeEventListener("resize", _updateDimensions)
window.removeEventListener("scrollend", _updatePosition) window.removeEventListener("scroll", _updatePosition)
} }
}, []) }, [])
@ -106,7 +110,7 @@ function getInitialPosition(canvasDimensions: typeof canvasDimentsion): ScrollTo
try { try {
initPosition = JSON.parse(localStorage.getItem("pos") ?? "") initPosition = JSON.parse(localStorage.getItem("pos") ?? "")
} catch (e) { } catch (e) {
console.error("no local storage for pos") console.error("no local storage for pos " + e)
} }
if (!initPosition) return defaultScroll if (!initPosition) return defaultScroll

View File

@ -2,14 +2,12 @@ import { Editor, Listener } from 'tiny-markdown-editor'
import './md.css' import './md.css'
import { useEffect, useRef } from "kaioken" import { useEffect, useRef } from "kaioken"
namespace MarkDownEditor { interface MarkDownEditorProps {
export interface Props { initial: string
initial: string onChange: Listener<'change'>
onChange: Listener<'change'>
}
} }
export function MarkDownEditor({ initial, onChange }: MarkDownEditor.Props) { export function MarkDownEditor({ initial, onChange }: MarkDownEditorProps) {
const elRef = useRef<HTMLDivElement>(null) const elRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {

View File

@ -24,7 +24,7 @@ export function MiniMap() {
useEffect(() => { useEffect(() => {
function _handleScroll(_e: Event) { function _handleScroll() {
scrollX.value = window.scrollX scrollX.value = window.scrollX
scrollY.value = window.scrollY scrollY.value = window.scrollY
} }
@ -47,7 +47,7 @@ export function MiniMap() {
const image = images.images.value[imageKey] const image = images.images.value[imageKey]
const el = useRef(null) const el = useRef(null)
function _handleItemClick(_e: MouseEvent) { function _handleItemClick() {
const newLeft = image.position.x - ((viewportWidth / 2) - (image.dimensions.w / 2)) const newLeft = image.position.x - ((viewportWidth / 2) - (image.dimensions.w / 2))
const newTop = image.position.y - ((viewportHeight / 2) - (image.dimensions.h / 2)) const newTop = image.position.y - ((viewportHeight / 2) - (image.dimensions.h / 2))
@ -65,7 +65,7 @@ export function MiniMap() {
const newZIndex = LayerEnum.MINIMAP + 1 const newZIndex = LayerEnum.MINIMAP + 1
return ( 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"} <div key={imageKey} 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} onclick={_handleItemClick}
style={{ style={{
width: `${newWidth}px`, width: `${newWidth}px`,
@ -82,7 +82,7 @@ export function MiniMap() {
{Object.keys(notes.notes.value).map((noteKey: NoteCardType['id']) => { {Object.keys(notes.notes.value).map((noteKey: NoteCardType['id']) => {
const note = notes.notes.value[noteKey] const note = notes.notes.value[noteKey]
function _handleItemClick(_e: MouseEvent) { function _handleItemClick() {
const newLeft = note.position.x - ((viewportWidth / 2) - (note.dimensions.w / 2)) const newLeft = note.position.x - ((viewportWidth / 2) - (note.dimensions.w / 2))
const newTop = note.position.y - ((viewportHeight / 2) - (note.dimensions.h / 2)) const newTop = note.position.y - ((viewportHeight / 2) - (note.dimensions.h / 2))
window.scrollTo({ window.scrollTo({
@ -99,7 +99,7 @@ export function MiniMap() {
const newZIndex = LayerEnum.MINIMAP + 1 const newZIndex = LayerEnum.MINIMAP + 1
return ( 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 key={noteKey} 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} onclick={_handleItemClick}
style={{ style={{
width: `${newWidth}px`, width: `${newWidth}px`,
@ -116,7 +116,7 @@ export function MiniMap() {
const text = texts.texts.value[textKey] const text = texts.texts.value[textKey]
const el = useRef(null) const el = useRef(null)
function _handleItemClick(_e: MouseEvent) { function _handleItemClick() {
window.scrollTo({ window.scrollTo({
left: text.position.x - ((viewportWidth / 2) - (text.dimensions.w / 2)), left: text.position.x - ((viewportWidth / 2) - (text.dimensions.w / 2)),
top: text.position.y - ((viewportHeight / 2) - (text.dimensions.h / 2)), top: text.position.y - ((viewportHeight / 2) - (text.dimensions.h / 2)),
@ -125,7 +125,7 @@ export function MiniMap() {
} }
return ( return (
<div ref={el} className={"bg-indigo-500 hover:bg-blue-500 cursor-pointer rounded"} <div key={textKey} ref={el} className={"bg-indigo-500 hover:bg-blue-500 cursor-pointer rounded"}
onclick={_handleItemClick} onclick={_handleItemClick}
style={{ style={{
position: 'absolute', position: 'absolute',

View File

@ -1,4 +1,4 @@
import { signal, useRef } from "kaioken" import { signal, useCallback, useEffect, useRef } from "kaioken"
import { NotesSigal, focusedItem } from "../signals" import { NotesSigal, focusedItem } from "../signals"
import { useDebounce } from "../utils/useDebounce" import { useDebounce } from "../utils/useDebounce"
import notes, { NoteCardType } from "../signals/notes" import notes, { NoteCardType } from "../signals/notes"
@ -10,15 +10,14 @@ import { Divider } from "./Divider"
import { ExportIcon } from "./icons/ExportIcon" import { ExportIcon } from "./icons/ExportIcon"
import { createFileAndExport } from "../utils/createFileAndExport" import { createFileAndExport } from "../utils/createFileAndExport"
import { ContextMenuPortal } from "./ContextMenuPortal" import { ContextMenuPortal } from "./ContextMenuPortal"
import { HelpIcon } from "./icons/HelpIcon"
namespace NoteCard { interface NoteCardProps {
export interface NoteCardProps { key: NoteCardType['id']
key: NoteCardType['id'] data: NoteCardType
data: NoteCardType
}
} }
export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) { export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
const saved = signal(true) const saved = signal(true)
const pressed = signal(false) const pressed = signal(false)
const newX = useRef(0) const newX = useRef(0)
@ -28,6 +27,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
const initialResizeX = useRef(0) const initialResizeX = useRef(0)
const initialResizeY = useRef(0) const initialResizeY = useRef(0)
const openContextMenu = signal(false) const openContextMenu = signal(false)
const showHelp = signal(false)
const { debounce } = useDebounce() const { debounce } = useDebounce()
@ -101,7 +101,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
saved.value = false saved.value = false
} }
function _handleClose(_e: Event) { function _handleClose() {
NotesSigal.default.removeNote(item.id) NotesSigal.default.removeNote(item.id)
NotesSigal.default.notes.notify() NotesSigal.default.notes.notify()
updateLocalStorage() updateLocalStorage()
@ -111,10 +111,14 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
focusedItem.value = itemKey focusedItem.value = itemKey
} }
function _handleExportClick(_e: MouseEvent) { function _exportFile() {
createFileAndExport("Note", item.contents, "text/markdown") createFileAndExport("Note", item.contents, "text/markdown")
} }
function _handleExportClick() {
_exportFile()
}
function _handleMouseClick(e: MouseEvent) { function _handleMouseClick(e: MouseEvent) {
e.preventDefault() e.preventDefault()
openContextMenu.value = !openContextMenu.value openContextMenu.value = !openContextMenu.value
@ -124,6 +128,49 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
openContextMenu.value = false openContextMenu.value = false
} }
function _handleHelpHover() {
if (showHelp.value) return
showHelp.value = true
}
function _handleHelpOut() {
if (!showHelp.value) return
showHelp.value = false
}
const _handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!e.ctrlKey) return
// TODO: add support for other os
// TODO: add modal popup
switch (e.key) {
case 'Delete':
e.preventDefault()
_handleClose()
break
case 'Backspace':
e.preventDefault()
_handleClose()
break
case 'e':
e.preventDefault()
_exportFile()
break
default:
break
}
}, [itemKey, item.position, NotesSigal.default])
useEffect(() => {
if (focusedItem.value !== itemKey) return
window.addEventListener('keydown', _handleKeyDown)
return () => {
window.removeEventListener('keydown', _handleKeyDown)
}
}, [focusedItem.value, itemKey])
const cardPositionStyle = { const cardPositionStyle = {
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`, zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
width: `${item.dimensions.w}px`, width: `${item.dimensions.w}px`,
@ -137,73 +184,94 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
} }
return ( return (
<div <>
oncontextmenu={_handleMouseClick} <div
onmousedown={_handleFocusCard} oncontextmenu={_handleMouseClick}
style={cardPositionStyle} onmousedown={_handleFocusCard}
className="overflow-hidden text-[#333] dark:bg-[#1a1a1a] dark:border-[#1c1c1c] bg-[#efeff0] select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#ddd] absolute" style={cardPositionStyle}
> className="overflow-hidden text-[#333] dark:bg-[#1a1a1a] dark:border-[#1c1c1c] bg-[#efeff0] 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="overflow-hidden flex-1 flex flex-col gap-1">
<div className="px-2 flex justify-between items-center cursor-move" onmousedown={_handleMouseDown}> {/* Header Bar */}
<div style={saveIndicatorStyle} className="rounded-full w-1 h-1 dark:bg-white bg-green-500"></div> <div className="px-2 flex justify-between items-center cursor-move" onmousedown={_handleMouseDown}>
<div className={"flex gap-1 items-center"}>
<div className="flex gap-2"> <HelpIcon onMouseOver={_handleHelpHover} onMouseOut={_handleHelpOut} />
<div {/* Save indicator*/}
onclick={_handleExportClick} <div style={saveIndicatorStyle} className="rounded-full w-1 h-1 dark:bg-white bg-green-500"></div>
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> </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 /> <Divider />
<button className="text-md dark:text-[#777] text-black" onclick={_handleClose}>x</button> <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>
<ContextMenuPortal open={openContextMenu.value} closeAction={_handleContextClose}>
<div className="bg-[#3c3c3c] flex flex-col rounded">
<div className="flex justify-between items-center">
<div className="text-md dark:text-[#999] text-black px-2 py-1">
{item.title}
</div> </div>
</div> </div>
<hr className="border dark:border-[#2c2c2c] border-[#ddd] m-0 p-0" /> <hr className="border dark:border-[#2c2c2c] border-[#ddd]" />
<div> {/* Content Body */}
<ul> <MarkDownEditor initial={item.contents} onChange={_handleMdChange} />
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1"> <ExpandIcon cb={_handleResizeMouseDown} />
<button onclick={_handleClose} className="text-md dark:text-[#999] text-black"> </div>
Delete
</button> <ContextMenuPortal open={openContextMenu.value} closeAction={_handleContextClose}>
</li> <div className="bg-[#3c3c3c] flex flex-col rounded">
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1"> <div className="flex justify-between items-center">
<button onclick={_handleExportClick} className="text-md dark:text-[#999] text-black"> <div className="text-md dark:text-[#999] text-black px-2 py-1">
export {item.title}
</button> </div>
</li> </div>
</ul>
<hr className="border dark:border-[#2c2c2c] border-[#ddd] m-0 p-0" />
<div>
<ul>
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1">
<button onclick={_handleClose} className="text-md dark:text-[#999] text-black">
Delete
</button>
</li>
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1">
<button onclick={_handleExportClick} className="text-md dark:text-[#999] text-black">
export
</button>
</li>
</ul>
</div>
</div>
</ContextMenuPortal>
</div >
{/* HOTKEY PAPER */}
{showHelp.value &&
<div
className={"dark:text-white absolute dark:bg-[#1c1c1c] bg-[#fff] rounded-md p-1 z-[1000] border border-blue-500"}
style={{
top: `${item.position.y}px`,
left: `${item.position.x - 120}px`
}}
>
<div className="flex flex-col text-sm dark:text-[#999] text-black">
<small>ctrl + del = delete</small>
<small>ctrl + back = delete</small>
<small>ctrl + e = export</small>
</div> </div>
</div> </div>
</ContextMenuPortal> }
</div > </>
) )
} }
function ExpandIcon({ cb }: { function ExpandIcon({ cb }: {
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined
}) { }) {
const isDarkTheme = useThemeDetector() const isDarkTheme = useThemeDetector()
@ -228,3 +296,4 @@ function ExpandIcon({ cb }: {
) )
} }

View File

@ -3,17 +3,15 @@ import { TextSignal, focusedItem } from "../signals"
import { useDebounce } from "../utils/useDebounce" import { useDebounce } from "../utils/useDebounce"
import texts, { TextCardType } from "../signals/texts" import texts, { TextCardType } from "../signals/texts"
import { LayerEnum } from "../utils/enums" import { LayerEnum } from "../utils/enums"
import { Card } from "../types" import { Card, CardTypes } from "../types"
import { useThemeDetector } from "../utils/useThemeDetector" import { useThemeDetector } from "../utils/useThemeDetector"
namespace TextItem { interface TextCardProps {
export interface TextCardProps { key: TextCardType['id']
key: TextCardType['id'] data: TextCardType
data: TextCardType
}
} }
export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) { export function TextItem({ key: itemKey, data: item }: TextCardProps) {
const { debounce } = useDebounce() const { debounce } = useDebounce()
const pressed = signal(false) const pressed = signal(false)
const newX = useRef(0) const newX = useRef(0)
@ -28,7 +26,7 @@ export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
const elDems = elRef.current?.getBoundingClientRect() const elDems = elRef.current?.getBoundingClientRect()
const elW = elDems?.width ?? 100 const elW = elDems?.width ?? 100
const elH = elDems?.height ?? 100 const elH = elDems?.height ?? 100
const newDems: Card<'texts'>['dimensions'] = { w: elW, h: elH } const newDems: Card<CardTypes.TEXTS>['dimensions'] = { w: elW, h: elH }
TextSignal.default.updateTextProperty(itemKey, 'dimensions', newDems) TextSignal.default.updateTextProperty(itemKey, 'dimensions', newDems)
TextSignal.default.texts.notify() TextSignal.default.texts.notify()
}, [elRef.current, item.fontSize]) }, [elRef.current, item.fontSize])
@ -37,7 +35,7 @@ export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
function updateLocalStorage(time?: number) { function updateLocalStorage(time?: number) {
debounce(() => { debounce(() => {
localStorage.setItem("texts", JSON.stringify(texts.texts.value)) localStorage.setItem(CardTypes.TEXTS, JSON.stringify(texts.texts.value))
}, time) }, time)
} }
@ -87,7 +85,7 @@ export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
window.addEventListener('mouseup', _handleResizeMouseUp) window.addEventListener('mouseup', _handleResizeMouseUp)
} }
function _handleResizeMouseUp(_e: MouseEvent) { function _handleResizeMouseUp() {
pressed.value = false pressed.value = false
window.removeEventListener('mousemove', _handleResizeMove) window.removeEventListener('mousemove', _handleResizeMove)
window.removeEventListener('mouseup', _handleResizeMouseUp) window.removeEventListener('mouseup', _handleResizeMouseUp)
@ -157,7 +155,7 @@ export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
> >
<p <p
ref={pRef} ref={pRef}
//@ts-expect-error //@ts-expect-error oninput doesnt exist on the props type
oninput={_handleContentInput} oninput={_handleContentInput}
contentEditable contentEditable
className={'text-p inline-block px-2 w-full select-none drop-shadow relative'}> className={'text-p inline-block px-2 w-full select-none drop-shadow relative'}>
@ -171,13 +169,12 @@ export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
) )
} }
namespace CloseIcon { interface CloseIconProps {
export interface Props { cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined,
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined, item: TextSignal.TextCardType
item: TextSignal.TextCardType
}
} }
function CloseIcon({ item, cb }: CloseIcon.Props) {
function CloseIcon({ item, cb }: CloseIconProps) {
const isDark = useThemeDetector() const isDark = useThemeDetector()
return ( return (
<svg <svg
@ -204,13 +201,12 @@ function CloseIcon({ item, cb }: CloseIcon.Props) {
) )
} }
namespace ExpandIcon { interface ExpandIconProps {
export interface Props { cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined,
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined, item: TextSignal.TextCardType
item: TextSignal.TextCardType
}
} }
function ExpandIcon({ cb, item }: ExpandIcon.Props) {
function ExpandIcon({ cb, item }: ExpandIconProps) {
const isDark = useThemeDetector() const isDark = useThemeDetector()
return ( return (
<svg <svg

View File

@ -8,6 +8,7 @@ import {
useEffect, useEffect,
useState, useState,
} from "kaioken" } from "kaioken"
import { noop } from "kaioken/utils"
type Toast = { type Toast = {
ts: number ts: number
@ -21,11 +22,15 @@ const defaultDuration = 3000
const ToastContext = createContext<{ const ToastContext = createContext<{
showToast: (type: Toast["type"], message: string) => void showToast: (type: Toast["type"], message: string) => void
}>(null as any) }>({ showToast: noop })
export const useToast = () => useContext(ToastContext) export const useToast = () => useContext(ToastContext)
export const ToastContextProvider: Kaioken.FC = ({ children }) => { interface ToastProviderProps {
children: JSX.Children
}
export function ToastContextProvider({ children }: ToastProviderProps) {
const [toasts, setToasts] = useState<Toast[]>([]) const [toasts, setToasts] = useState<Toast[]>([])
useEffect(() => { useEffect(() => {

View File

@ -1,5 +1,6 @@
import { ImagesSignal } from "../../signals" import { ImagesSignal } from "../../signals"
import images from "../../signals/images" import images from "../../signals/images"
import { CardTypes } from "../../types"
import { updateLocalStorage } from "../../utils/localStorage" import { updateLocalStorage } from "../../utils/localStorage"
import { useToast } from "../Toast" import { useToast } from "../Toast"
import { Tooltip } from "./Tooltip" import { Tooltip } from "./Tooltip"
@ -13,12 +14,16 @@ export function ImageCardButton() {
input.accept = "image/*" input.accept = "image/*"
input.multiple = false input.multiple = false
input.onchange = (e: any) => { input.onchange = (e: Event) => {
const file = e.target.files[0] const el = e.target as HTMLInputElement
if (!el.files?.length) return
if (!el.files.length) return
const file = el.files[0]
const reader = new FileReader() const reader = new FileReader()
reader.readAsDataURL(file) reader.readAsDataURL(file)
reader.onload = function(readerEvent) { reader.onload = function(readerEvent) {
let image = document.createElement('img') const image = document.createElement('img')
image.onload = function() { image.onload = function() {
const { width, height } = image const { width, height } = image
@ -33,7 +38,7 @@ export function ImageCardButton() {
if (!img) return if (!img) return
const imgId = ImagesSignal.default.addImage({ const imgId = ImagesSignal.default.addImage({
type: "image", type: CardTypes.IMAGES,
title: "New Image", title: "New Image",
contents: content as string, contents: content as string,
position: { position: {
@ -47,7 +52,7 @@ export function ImageCardButton() {
}) })
try { try {
updateLocalStorage("images", images.images.value) updateLocalStorage(CardTypes.IMAGES, images.images).notify()
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof DOMException) { if (e instanceof DOMException) {
if (e.name !== 'QuotaExceededError') return if (e.name !== 'QuotaExceededError') return

View File

@ -1,44 +1,58 @@
import images from "../../signals/images" import images, { ImageCardType } from "../../signals/images"
import notes from "../../signals/notes" import notes, { NoteCardType } from "../../signals/notes"
import { Card } from "../../types" import texts, { TextCardType } from "../../signals/texts"
import { Card, CardTypes } from "../../types"
import { convertBase64ToJson } from "../../utils/convertBase64ToJson" import { convertBase64ToJson } from "../../utils/convertBase64ToJson"
import { updateLocalStorage } from "../../utils/localStorage" import { updateLocalStorage } from "../../utils/localStorage"
import { Tooltip } from "./Tooltip" import { Tooltip } from "./Tooltip"
import { defaultClassName } from "./utils" import { defaultClassName } from "./utils"
type legacySupportTypes = { type: CardTypes | string }
export function ImportButton() { export function ImportButton() {
function _handleImport() { function _handleImport() {
// guard clause to prevent overwriting existing cards
if (Object.keys(images.images.value).length || Object.keys(notes.notes.value).length) {
const isConfirmed = confirm("Are you sure you want to overwrite your existing cards?")
if (!isConfirmed) return
}
const input = document.createElement('input') const input = document.createElement('input')
input.type = 'file' input.type = 'file'
input.accept = ".json" input.accept = ".json"
input.multiple = false input.multiple = false
input.onchange = (e: any) => { input.onchange = (e: Event) => {
const file = e.target.files[0] if (e.target === null) return
const el = e.target as HTMLInputElement
if (!el.files?.length) return
const file = el.files[0]
const reader = new FileReader() const reader = new FileReader()
reader.readAsDataURL(file) reader.readAsDataURL(file)
reader.onload = function(readerEvent) { reader.onload = function(readerEvent) {
let content = readerEvent.target?.result; let content = readerEvent.target?.result;
// get only the base64 parts and not any identifiers // get only the base64 parts and not any identifiers
content = (content as string).split(',')[1] content = (content as string).split(',')[1]
const data: Record<string, Card<'notes'> | Card<'images'>> = convertBase64ToJson(content) const data: Record<string, Omit<Card<CardTypes>, 'type'> & legacySupportTypes> = convertBase64ToJson(content)
for (let key in data) { for (const key in data) {
const item = data[key] const item = data[key]
if (item.type == 'images') { const { id: _, ...rest } = item
const { id, ...rest } = item if (item.type === CardTypes.IMAGES || item.type == 'image') {
images.addImage(rest) images.addImage(rest as ImageCardType)
} continue
} else if (item.type === CardTypes.NOTES || item.type === 'note') {
if (item.type == 'notes') { notes.addNote(rest as NoteCardType)
const { id, ...rest } = item continue
notes.addNote(rest) } else if (item.type === CardTypes.TEXTS || item.type === 'text') {
texts.addText(rest as TextCardType)
continue
} }
} }
updateLocalStorage('notes', notes.notes.value) updateLocalStorage(CardTypes.NOTES, notes.notes).notify()
updateLocalStorage('images', images.images.value) updateLocalStorage(CardTypes.IMAGES, images.images).notify()
notes.notes.notify() updateLocalStorage(CardTypes.TEXTS, texts.texts).notify()
images.images.notify()
} }
} }
input.click() input.click()

View File

@ -1,5 +1,6 @@
import { NotesSigal } from "../../signals" import { NotesSigal } from "../../signals"
import notes from "../../signals/notes" import notes from "../../signals/notes"
import { CardTypes } from "../../types"
import { updateLocalStorage } from "../../utils/localStorage" import { updateLocalStorage } from "../../utils/localStorage"
import { Tooltip } from "./Tooltip" import { Tooltip } from "./Tooltip"
import { defaultClassName } from "./utils" import { defaultClassName } from "./utils"
@ -7,7 +8,7 @@ import { defaultClassName } from "./utils"
export function StickyNoteButton() { export function StickyNoteButton() {
function _handleClick(e: MouseEvent) { function _handleClick(e: MouseEvent) {
NotesSigal.default.addNote({ NotesSigal.default.addNote({
type: "note", type: CardTypes.NOTES,
title: "New Note", title: "New Note",
contents: "", contents: "",
position: { position: {
@ -19,7 +20,7 @@ export function StickyNoteButton() {
h: 200 h: 200
} }
}) })
updateLocalStorage("notes", notes.notes.value) updateLocalStorage(CardTypes.NOTES, notes.notes)
} }
return ( return (

View File

@ -3,13 +3,14 @@ import { TextSignal } from "../../signals";
import texts from "../../signals/texts"; import texts from "../../signals/texts";
import { updateLocalStorage } from "../../utils/localStorage"; import { updateLocalStorage } from "../../utils/localStorage";
import { defaultClassName } from "./utils"; import { defaultClassName } from "./utils";
import { CardTypes } from "../../types";
export function TextButton() { export function TextButton() {
function _handleClick(e: MouseEvent) { function _handleClick(e: MouseEvent) {
TextSignal.default.addText({ TextSignal.default.addText({
fontSize: 84, fontSize: 84,
type: "texts", type: CardTypes.TEXTS,
title: "New Note", title: "New Note",
contents: "todo: fill me", contents: "todo: fill me",
position: { position: {
@ -21,7 +22,7 @@ export function TextButton() {
h: 100 h: 100
} }
}) })
updateLocalStorage("texts", texts.texts.value) updateLocalStorage(CardTypes.TEXTS, texts.texts).notify()
} }
return ( return (

View File

@ -1,9 +1,8 @@
namespace ExportIcon { interface ExportIconProps {
export interface Props { className?: string
className?: string
}
} }
export function ExportIcon({ className }: ExportIcon.Props) {
export function ExportIcon({ className }: ExportIconProps) {
return ( return (
<svg <svg

View File

@ -0,0 +1,34 @@
interface HelpIconProps {
onMouseOver?: () => void
onMouseOut?: () => void
}
export function HelpIcon({ onMouseOver, onMouseOut }: HelpIconProps) {
return (
<svg
onmouseout={onMouseOut}
onmouseover={onMouseOver}
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="dark:text-[#5c5c5c] cursor-pointer w-4 h-4 text-[#9c9c9c] hover:text-blue-500 transition-color duration-300"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path
d="M12 17h.01"
/>
</svg>)
}

View File

@ -1,4 +1 @@
export * from "./CanvasItem"
export * from "./CanvasControls"
export * from "./InfinateCanvas" export * from "./InfinateCanvas"
export * from "./CardSelector"

View File

@ -1,8 +1,8 @@
import { signal } from "kaioken" import { signal } from "kaioken"
import { Card } from "../types" import { Card, CardTypes } from "../types"
import { focusedItem } from "." import { focusedItem } from "."
export type ImageCardType = Card<"images"> export type ImageCardType = Card<CardTypes.IMAGES>
const images = signal<Record<ImageCardType["id"], ImageCardType>>({}) const images = signal<Record<ImageCardType["id"], ImageCardType>>({})
@ -15,6 +15,7 @@ function addImage(data: Omit<ImageCardType, "id">) {
...data, ...data,
id: crypto.randomUUID(), id: crypto.randomUUID(),
} }
console.log("adding image: ", newCard)
images.value[newCard.id] = newCard images.value[newCard.id] = newCard
images.notify() images.notify()
focusedItem.value = newCard.id focusedItem.value = newCard.id

View File

@ -1,8 +1,8 @@
import { signal } from "kaioken" import { signal } from "kaioken"
import { Card } from "../types" import { Card, CardTypes } from "../types"
import { focusedItem } from "." import { focusedItem } from "."
export type NoteCardType = Card<"notes"> export type NoteCardType = Card<CardTypes.NOTES>
const notes = signal<Record<NoteCardType["id"], NoteCardType>>({}) const notes = signal<Record<NoteCardType["id"], NoteCardType>>({})

View File

@ -1,8 +1,8 @@
import { signal } from "kaioken" import { signal } from "kaioken"
import { Card } from "../types" import { Card, CardTypes } from "../types"
import { focusedItem } from "." import { focusedItem } from "."
export type TextCardType = Card<"texts"> & { export type TextCardType = Card<CardTypes.TEXTS> & {
fontSize: number fontSize: number
} }

View File

@ -1,14 +1,19 @@
export type CardTypes = "notes" | "images" | "texts"
export type positionCoords = { x: number; y: number } export type positionCoords = { x: number; y: number }
export type dimensionCoords = { w: number; h: number } export type dimensionCoords = { w: number; h: number }
export enum CardTypes {
NOTES = "notes",
IMAGES = "images",
TEXTS = "texts",
}
type Base64 = string type Base64 = string
export interface Card<Ttype extends CardTypes> { export interface Card<TCard extends CardTypes> {
id: string id: string
type: Ttype type: TCard
title: string title: string
contents: Ttype extends "image" ? Base64 : string contents: TCard extends "image" ? Base64 : string
position: positionCoords position: positionCoords
dimensions: dimensionCoords dimensions: dimensionCoords
} }

View File

@ -1,8 +1,17 @@
import { CardTypes } from "../types" import { Signal } from "kaioken"
import { Card, CardTypes } from "../types"
export function updateLocalStorage( export function updateLocalStorage(
location: CardTypes, location: CardTypes,
collection: unknown[] | Record<string, unknown> collection: Signal<Record<string, Card<CardTypes>>>
) { ) {
localStorage.setItem(location, JSON.stringify(collection)) try {
localStorage.setItem(location, JSON.stringify(collection.value))
} catch (e) {
throw new DOMException(
"Could not update local storage " + e,
"LocalStorageError"
)
}
return collection
} }

View File

@ -2,12 +2,12 @@ import { sideEffectsEnabled, useHook } from "kaioken"
import { noop } from "kaioken/utils" import { noop } from "kaioken/utils"
type UseDebounceState = { type UseDebounceState = {
timer: number timer: NodeJS.Timeout | undefined
debounce: (this: any, func: Function, timeout?: number) => void debounce: (func: (...args: unknown[]) => void, timeout?: number) => void
} }
function createState(): UseDebounceState { function createState(): UseDebounceState {
return { timer: 0, debounce: noop } return { timer: undefined, debounce: noop }
} }
export function useDebounce() { export function useDebounce() {
@ -17,8 +17,7 @@ export function useDebounce() {
if (!isInit) return { timer: hook.timer, debounce: hook.debounce } if (!isInit) return { timer: hook.timer, debounce: hook.debounce }
hook.debounce = function debounce( hook.debounce = function debounce(
this: any, func: (...args: unknown[]) => void,
func: Function,
timeout = 300 timeout = 300
) { ) {
clearTimeout(hook.timer) clearTimeout(hook.timer)

View File

@ -1,5 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
@ -15,8 +17,6 @@
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"jsx": "preserve" "jsx": "preserve"
}, },

View File

@ -1,9 +1,25 @@
import { defineConfig } from "vite" import { defineConfig } from "vite"
import kaioken from "vite-plugin-kaioken" import kaioken from "vite-plugin-kaioken"
import eslint from "vite-plugin-eslint"
import checker from "vite-plugin-checker"
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [kaioken()], plugins: [
kaioken(),
eslint({
cache: true,
ignorePatterns: ["src-tauri"],
emitError: true,
emitWarning: true,
failOnError: false,
failOnWarning: false,
lintOnStart: true,
}),
checker({
typescript: true,
}),
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// //
// 1. prevent vite from obscuring rust errors // 1. prevent vite from obscuring rust errors