generated from Klectr/KTemplate
Compare commits
No commits in common. "main" and "feat/text-support" have entirely different histories.
main
...
feat/text-
87
README.md
87
README.md
@ -1,86 +1,7 @@
|
|||||||
### **Kslab**
|
# Tauri + Vanilla TS
|
||||||
|
|
||||||
A visual organization tool for notes, text, shapes, and images.
|
This template should help get you started developing with [Tauri](https://tauri.app) & [Kaioken](https://kaioken.dev).
|
||||||
|
|
||||||
### **Table of Contents**
|
## Recommended IDE Setup
|
||||||
|
|
||||||
* [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)
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
@ -11,29 +11,20 @@
|
|||||||
"commit": "cz"
|
"commit": "cz"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kaioken-core/hooks": "^0.0.8",
|
|
||||||
"@tauri-apps/api": "^1",
|
"@tauri-apps/api": "^1",
|
||||||
"caniuse-lite": "^1.0.30001667",
|
"caniuse-lite": "^1.0.30001667",
|
||||||
"kaioken": "^0.32.1",
|
"kaioken": "^0.32.1",
|
||||||
"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": {
|
||||||
|
@ -9,7 +9,6 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<ToastContextProvider>
|
<ToastContextProvider>
|
||||||
<MemoInfyCanvas />
|
<MemoInfyCanvas />
|
||||||
<div id="context-menu-portal" />
|
|
||||||
</ToastContextProvider>
|
</ToastContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import { useClickOutside, useMouse } from "@kaioken-core/hooks";
|
|
||||||
import { Portal, signal, useEffect, useRef } from "kaioken";
|
|
||||||
|
|
||||||
interface ContextMenuPortalProps {
|
|
||||||
children: JSX.Children
|
|
||||||
open: boolean
|
|
||||||
closeAction: (() => void) | null | undefined
|
|
||||||
}
|
|
||||||
export function ContextMenuPortal({ children, open, closeAction }: ContextMenuPortalProps) {
|
|
||||||
const { mouse } = useMouse()
|
|
||||||
const pos = signal({ x: 0, y: 0 })
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
useClickOutside(ref, () => {
|
|
||||||
closeAction?.()
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return
|
|
||||||
pos.value = { x: mouse.x, y: mouse.y }
|
|
||||||
function _handleEscapeKey(e: KeyboardEvent) {
|
|
||||||
if (e.key !== "Escape") return
|
|
||||||
closeAction?.()
|
|
||||||
}
|
|
||||||
document.addEventListener("keydown", _handleEscapeKey)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", _handleEscapeKey)
|
|
||||||
}
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
if (!open) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Portal
|
|
||||||
container={() => document.querySelector("#context-menu-portal")!}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="fixed z-10 select-none shadow-md rounded min-w-[150px]"
|
|
||||||
style={{
|
|
||||||
left: `${pos.value.x}px`,
|
|
||||||
top: `${pos.value.y}px`,
|
|
||||||
zIndex: `1000`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
)
|
|
||||||
}
|
|
@ -5,15 +5,15 @@ import { LayerEnum } from "../utils/enums"
|
|||||||
import images, { ImageCardType } from "../signals/images"
|
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 { CardTypes } from "../types"
|
|
||||||
|
|
||||||
interface ImageCardProps {
|
namespace ImageCard {
|
||||||
key: ImageCardType['id']
|
export interface ImageCardProps {
|
||||||
data: ImageCardType
|
key: ImageCardType['id']
|
||||||
|
data: ImageCardType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageCard({ key: itemKey, data: item }: ImageCardProps) {
|
export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps) {
|
||||||
const { debounce } = useDebounce()
|
const { debounce } = useDebounce()
|
||||||
const pressed = signal(false)
|
const pressed = signal(false)
|
||||||
const newX = useRef(0)
|
const newX = useRef(0)
|
||||||
@ -22,11 +22,10 @@ export function ImageCard({ key: itemKey, data: item }: ImageCardProps) {
|
|||||||
const offsetY = useRef(0)
|
const offsetY = useRef(0)
|
||||||
const initialResizeX = useRef(0)
|
const initialResizeX = useRef(0)
|
||||||
const initialResizeY = useRef(0)
|
const initialResizeY = useRef(0)
|
||||||
const openContextMenu = signal(false)
|
|
||||||
|
|
||||||
function debounceLSUpdate(time?: number) {
|
function debounceLSUpdate(time?: number) {
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
updateLocalStorage(CardTypes.IMAGES, images.images).notify()
|
updateLocalStorage("images", images.images.value)
|
||||||
}, time)
|
}, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,78 +88,41 @@ export function ImageCard({ key: itemKey, data: item }: ImageCardProps) {
|
|||||||
window.removeEventListener('mouseup', _handleResizeMouseUp)
|
window.removeEventListener('mouseup', _handleResizeMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleClose() {
|
function _handleClose(_e: Event) {
|
||||||
ImagesSignal.default.removeImage(item.id)
|
ImagesSignal.default.removeImage(item.id)
|
||||||
ImagesSignal.default.images.notify()
|
ImagesSignal.default.images.notify()
|
||||||
debounceLSUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _handleMouseClick(e: MouseEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
openContextMenu.value = !openContextMenu.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function _handleContextClose() {
|
|
||||||
openContextMenu.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
onmousedown={_handleMouseDown}
|
||||||
oncontextmenu={_handleMouseClick}
|
className="select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#1c1c1c] absolute"
|
||||||
onmousedown={_handleMouseDown}
|
style={{
|
||||||
className="select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#1c1c1c] absolute"
|
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
||||||
style={{
|
top: `${item.position.y}px`,
|
||||||
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
left: `${item.position.x}px`,
|
||||||
top: `${item.position.y}px`,
|
width: `${item.dimensions.w}px`,
|
||||||
left: `${item.position.x}px`,
|
height: `${item.dimensions.h}px`,
|
||||||
width: `${item.dimensions.w}px`,
|
backgroundColor: '#181818',
|
||||||
height: `${item.dimensions.h}px`,
|
backgroundImage: `url(${item.contents})`,
|
||||||
backgroundColor: '#181818',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundImage: `url(${item.contents})`,
|
backgroundSize: 'cover',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundPosition: 'center'
|
||||||
backgroundSize: 'cover',
|
}}
|
||||||
backgroundPosition: 'center'
|
>
|
||||||
}}
|
<button
|
||||||
>
|
className="flex justify-center items-center hover:bg-blue-500 rounded w-5 h-5 dark:text-[#777] dark:hover:text-white text-white text-md absolute right-0 top-0"
|
||||||
<button
|
onclick={_handleClose}>x</button>
|
||||||
className="flex justify-center items-center hover:bg-blue-500 rounded w-5 h-5 dark:text-[#777] dark:hover:text-white text-white text-md absolute right-0 top-0"
|
|
||||||
onclick={_handleClose}>x</button>
|
|
||||||
|
|
||||||
<ExpandIcon cb={_handleResizeMouseDown} />
|
<ExpandIcon cb={_handleResizeMouseDown} />
|
||||||
</div >
|
</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>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ContextMenuPortal>
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ExpandIcon({ cb }: {
|
function ExpandIcon({ cb }: {
|
||||||
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined
|
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined
|
||||||
}) {
|
}) {
|
||||||
const isDarkTheme = useThemeDetector()
|
const isDarkTheme = useThemeDetector()
|
||||||
return (
|
return (
|
||||||
|
@ -10,12 +10,10 @@ 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)
|
||||||
@ -29,24 +27,22 @@ export default function InfiniteCanvas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _updatePosition() {
|
function _updatePosition() {
|
||||||
debounce(() => {
|
localStorage.setItem("pos", JSON.stringify({
|
||||||
localStorage.setItem("pos", JSON.stringify({
|
left: window.scrollX,
|
||||||
left: window.scrollX,
|
top: window.scrollY
|
||||||
top: window.scrollY
|
}))
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateDimensions()
|
_updateDimensions()
|
||||||
window.addEventListener("resize", _updateDimensions)
|
window.addEventListener("resize", _updateDimensions)
|
||||||
window.addEventListener("scroll", _updatePosition)
|
window.addEventListener("scrollend", _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("scroll", _updatePosition)
|
window.removeEventListener("scrollend", _updatePosition)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -110,7 +106,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 " + e)
|
console.error("no local storage for pos")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initPosition) return defaultScroll
|
if (!initPosition) return defaultScroll
|
||||||
|
@ -2,12 +2,14 @@ import { Editor, Listener } from 'tiny-markdown-editor'
|
|||||||
import './md.css'
|
import './md.css'
|
||||||
import { useEffect, useRef } from "kaioken"
|
import { useEffect, useRef } from "kaioken"
|
||||||
|
|
||||||
interface MarkDownEditorProps {
|
namespace MarkDownEditor {
|
||||||
initial: string
|
export interface Props {
|
||||||
onChange: Listener<'change'>
|
initial: string
|
||||||
|
onChange: Listener<'change'>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkDownEditor({ initial, onChange }: MarkDownEditorProps) {
|
export function MarkDownEditor({ initial, onChange }: MarkDownEditor.Props) {
|
||||||
const elRef = useRef<HTMLDivElement>(null)
|
const elRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -24,7 +24,7 @@ export function MiniMap() {
|
|||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function _handleScroll() {
|
function _handleScroll(_e: Event) {
|
||||||
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() {
|
function _handleItemClick(_e: MouseEvent) {
|
||||||
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 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"}
|
<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}
|
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() {
|
function _handleItemClick(_e: MouseEvent) {
|
||||||
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 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"}
|
<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}
|
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() {
|
function _handleItemClick(_e: MouseEvent) {
|
||||||
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 key={textKey} ref={el} className={"bg-indigo-500 hover:bg-blue-500 cursor-pointer rounded"}
|
<div ref={el} className={"bg-indigo-500 hover:bg-blue-500 cursor-pointer rounded"}
|
||||||
onclick={_handleItemClick}
|
onclick={_handleItemClick}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { signal, useCallback, useEffect, useRef } from "kaioken"
|
import { signal, 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"
|
||||||
@ -9,15 +9,15 @@ import { ChangeEvent } from "tiny-markdown-editor"
|
|||||||
import { Divider } from "./Divider"
|
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 { HelpIcon } from "./icons/HelpIcon"
|
|
||||||
|
|
||||||
interface NoteCardProps {
|
namespace NoteCard {
|
||||||
key: NoteCardType['id']
|
export interface NoteCardProps {
|
||||||
data: NoteCardType
|
key: NoteCardType['id']
|
||||||
|
data: NoteCardType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
|
export function NoteCard({ key: itemKey, data: item }: NoteCard.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)
|
||||||
@ -26,8 +26,6 @@ export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
|
|||||||
const offsetY = useRef(0)
|
const offsetY = useRef(0)
|
||||||
const initialResizeX = useRef(0)
|
const initialResizeX = useRef(0)
|
||||||
const initialResizeY = useRef(0)
|
const initialResizeY = useRef(0)
|
||||||
const openContextMenu = signal(false)
|
|
||||||
const showHelp = signal(false)
|
|
||||||
|
|
||||||
const { debounce } = useDebounce()
|
const { debounce } = useDebounce()
|
||||||
|
|
||||||
@ -101,7 +99,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
|
|||||||
saved.value = false
|
saved.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleClose() {
|
function _handleClose(_e: Event) {
|
||||||
NotesSigal.default.removeNote(item.id)
|
NotesSigal.default.removeNote(item.id)
|
||||||
NotesSigal.default.notes.notify()
|
NotesSigal.default.notes.notify()
|
||||||
updateLocalStorage()
|
updateLocalStorage()
|
||||||
@ -111,66 +109,10 @@ export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
|
|||||||
focusedItem.value = itemKey
|
focusedItem.value = itemKey
|
||||||
}
|
}
|
||||||
|
|
||||||
function _exportFile() {
|
function _handleExportClick(_e: MouseEvent) {
|
||||||
createFileAndExport("Note", item.contents, "text/markdown")
|
createFileAndExport("Note", item.contents, "text/markdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleExportClick() {
|
|
||||||
_exportFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _handleMouseClick(e: MouseEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
openContextMenu.value = !openContextMenu.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function _handleContextClose() {
|
|
||||||
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`,
|
||||||
@ -184,94 +126,45 @@ export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<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"}>
|
|
||||||
<HelpIcon onMouseOver={_handleHelpHover} onMouseOut={_handleHelpOut} />
|
|
||||||
{/* Save indicator*/}
|
|
||||||
<div style={saveIndicatorStyle} className="rounded-full w-1 h-1 dark:bg-white bg-green-500"></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 />
|
<div className="flex gap-2">
|
||||||
|
<div
|
||||||
<button className="text-md dark:text-[#777] text-black" onclick={_handleClose}>x</button>
|
onclick={_handleExportClick}
|
||||||
|
className="flex items-center">
|
||||||
</div>
|
<ExportIcon
|
||||||
</div>
|
className="dark:text-[#5c5c5c] cursor-pointer w-4 h-4 text-[#9c9c9c] hover:text-blue-500 transition-color duration-300"
|
||||||
|
/>
|
||||||
<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>
|
||||||
|
|
||||||
<hr className="border dark:border-[#2c2c2c] border-[#ddd] m-0 p-0" />
|
<Divider />
|
||||||
|
|
||||||
<div>
|
<button className="text-md dark:text-[#777] text-black" onclick={_handleClose}>x</button>
|
||||||
<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>
|
||||||
}
|
|
||||||
</>
|
<hr className="border dark:border-[#2c2c2c] border-[#ddd]" />
|
||||||
|
|
||||||
|
{/* Content Body */}
|
||||||
|
<MarkDownEditor initial={item.contents} onChange={_handleMdChange} />
|
||||||
|
<ExpandIcon cb={_handleResizeMouseDown} />
|
||||||
|
</div>
|
||||||
|
</div >
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpandIcon({ cb }: {
|
function ExpandIcon({ cb }: {
|
||||||
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined
|
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined
|
||||||
}) {
|
}) {
|
||||||
const isDarkTheme = useThemeDetector()
|
const isDarkTheme = useThemeDetector()
|
||||||
|
|
||||||
@ -296,4 +189,3 @@ function ExpandIcon({ cb }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,15 +3,17 @@ 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, CardTypes } from "../types"
|
import { Card } from "../types"
|
||||||
import { useThemeDetector } from "../utils/useThemeDetector"
|
import { useThemeDetector } from "../utils/useThemeDetector"
|
||||||
|
|
||||||
interface TextCardProps {
|
namespace TextItem {
|
||||||
key: TextCardType['id']
|
export interface TextCardProps {
|
||||||
data: TextCardType
|
key: TextCardType['id']
|
||||||
|
data: TextCardType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextItem({ key: itemKey, data: item }: TextCardProps) {
|
export function TextItem({ key: itemKey, data: item }: TextItem.TextCardProps) {
|
||||||
const { debounce } = useDebounce()
|
const { debounce } = useDebounce()
|
||||||
const pressed = signal(false)
|
const pressed = signal(false)
|
||||||
const newX = useRef(0)
|
const newX = useRef(0)
|
||||||
@ -26,7 +28,7 @@ export function TextItem({ key: itemKey, data: item }: 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<CardTypes.TEXTS>['dimensions'] = { w: elW, h: elH }
|
const newDems: Card<'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])
|
||||||
@ -35,7 +37,7 @@ export function TextItem({ key: itemKey, data: item }: TextCardProps) {
|
|||||||
|
|
||||||
function updateLocalStorage(time?: number) {
|
function updateLocalStorage(time?: number) {
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
localStorage.setItem(CardTypes.TEXTS, JSON.stringify(texts.texts.value))
|
localStorage.setItem("texts", JSON.stringify(texts.texts.value))
|
||||||
}, time)
|
}, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +87,7 @@ export function TextItem({ key: itemKey, data: item }: TextCardProps) {
|
|||||||
window.addEventListener('mouseup', _handleResizeMouseUp)
|
window.addEventListener('mouseup', _handleResizeMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleResizeMouseUp() {
|
function _handleResizeMouseUp(_e: MouseEvent) {
|
||||||
pressed.value = false
|
pressed.value = false
|
||||||
window.removeEventListener('mousemove', _handleResizeMove)
|
window.removeEventListener('mousemove', _handleResizeMove)
|
||||||
window.removeEventListener('mouseup', _handleResizeMouseUp)
|
window.removeEventListener('mouseup', _handleResizeMouseUp)
|
||||||
@ -155,7 +157,7 @@ export function TextItem({ key: itemKey, data: item }: TextCardProps) {
|
|||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
ref={pRef}
|
ref={pRef}
|
||||||
//@ts-expect-error oninput doesnt exist on the props type
|
//@ts-expect-error
|
||||||
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'}>
|
||||||
@ -169,12 +171,13 @@ export function TextItem({ key: itemKey, data: item }: TextCardProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CloseIconProps {
|
namespace CloseIcon {
|
||||||
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined,
|
export interface Props {
|
||||||
item: TextSignal.TextCardType
|
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined,
|
||||||
|
item: TextSignal.TextCardType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
function CloseIcon({ item, cb }: CloseIcon.Props) {
|
||||||
function CloseIcon({ item, cb }: CloseIconProps) {
|
|
||||||
const isDark = useThemeDetector()
|
const isDark = useThemeDetector()
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -201,12 +204,13 @@ function CloseIcon({ item, cb }: CloseIconProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExpandIconProps {
|
namespace ExpandIcon {
|
||||||
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined,
|
export interface Props {
|
||||||
item: TextSignal.TextCardType
|
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined,
|
||||||
|
item: TextSignal.TextCardType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
function ExpandIcon({ cb, item }: ExpandIcon.Props) {
|
||||||
function ExpandIcon({ cb, item }: ExpandIconProps) {
|
|
||||||
const isDark = useThemeDetector()
|
const isDark = useThemeDetector()
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "kaioken"
|
} from "kaioken"
|
||||||
import { noop } from "kaioken/utils"
|
|
||||||
|
|
||||||
type Toast = {
|
type Toast = {
|
||||||
ts: number
|
ts: number
|
||||||
@ -22,15 +21,11 @@ const defaultDuration = 3000
|
|||||||
|
|
||||||
const ToastContext = createContext<{
|
const ToastContext = createContext<{
|
||||||
showToast: (type: Toast["type"], message: string) => void
|
showToast: (type: Toast["type"], message: string) => void
|
||||||
}>({ showToast: noop })
|
}>(null as any)
|
||||||
|
|
||||||
export const useToast = () => useContext(ToastContext)
|
export const useToast = () => useContext(ToastContext)
|
||||||
|
|
||||||
interface ToastProviderProps {
|
export const ToastContextProvider: Kaioken.FC = ({ children }) => {
|
||||||
children: JSX.Children
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ToastContextProvider({ children }: ToastProviderProps) {
|
|
||||||
const [toasts, setToasts] = useState<Toast[]>([])
|
const [toasts, setToasts] = useState<Toast[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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"
|
||||||
@ -14,16 +13,12 @@ export function ImageCardButton() {
|
|||||||
input.accept = "image/*"
|
input.accept = "image/*"
|
||||||
input.multiple = false
|
input.multiple = false
|
||||||
|
|
||||||
input.onchange = (e: Event) => {
|
input.onchange = (e: any) => {
|
||||||
const el = e.target as HTMLInputElement
|
const file = e.target.files[0]
|
||||||
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) {
|
||||||
const image = document.createElement('img')
|
let image = document.createElement('img')
|
||||||
image.onload = function() {
|
image.onload = function() {
|
||||||
const { width, height } = image
|
const { width, height } = image
|
||||||
|
|
||||||
@ -38,7 +33,7 @@ export function ImageCardButton() {
|
|||||||
if (!img) return
|
if (!img) return
|
||||||
|
|
||||||
const imgId = ImagesSignal.default.addImage({
|
const imgId = ImagesSignal.default.addImage({
|
||||||
type: CardTypes.IMAGES,
|
type: "image",
|
||||||
title: "New Image",
|
title: "New Image",
|
||||||
contents: content as string,
|
contents: content as string,
|
||||||
position: {
|
position: {
|
||||||
@ -52,7 +47,7 @@ export function ImageCardButton() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateLocalStorage(CardTypes.IMAGES, images.images).notify()
|
updateLocalStorage("images", images.images.value)
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof DOMException) {
|
if (e instanceof DOMException) {
|
||||||
if (e.name !== 'QuotaExceededError') return
|
if (e.name !== 'QuotaExceededError') return
|
||||||
|
@ -1,58 +1,44 @@
|
|||||||
import images, { ImageCardType } from "../../signals/images"
|
import images from "../../signals/images"
|
||||||
import notes, { NoteCardType } from "../../signals/notes"
|
import notes from "../../signals/notes"
|
||||||
import texts, { TextCardType } from "../../signals/texts"
|
import { Card } from "../../types"
|
||||||
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: Event) => {
|
input.onchange = (e: any) => {
|
||||||
if (e.target === null) return
|
const file = e.target.files[0]
|
||||||
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, Omit<Card<CardTypes>, 'type'> & legacySupportTypes> = convertBase64ToJson(content)
|
const data: Record<string, Card<'notes'> | Card<'images'>> = convertBase64ToJson(content)
|
||||||
for (const key in data) {
|
for (let key in data) {
|
||||||
const item = data[key]
|
const item = data[key]
|
||||||
const { id: _, ...rest } = item
|
if (item.type == 'images') {
|
||||||
if (item.type === CardTypes.IMAGES || item.type == 'image') {
|
const { id, ...rest } = item
|
||||||
images.addImage(rest as ImageCardType)
|
images.addImage(rest)
|
||||||
continue
|
}
|
||||||
} else if (item.type === CardTypes.NOTES || item.type === 'note') {
|
|
||||||
notes.addNote(rest as NoteCardType)
|
if (item.type == 'notes') {
|
||||||
continue
|
const { id, ...rest } = item
|
||||||
} else if (item.type === CardTypes.TEXTS || item.type === 'text') {
|
notes.addNote(rest)
|
||||||
texts.addText(rest as TextCardType)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalStorage(CardTypes.NOTES, notes.notes).notify()
|
updateLocalStorage('notes', notes.notes.value)
|
||||||
updateLocalStorage(CardTypes.IMAGES, images.images).notify()
|
updateLocalStorage('images', images.images.value)
|
||||||
updateLocalStorage(CardTypes.TEXTS, texts.texts).notify()
|
notes.notes.notify()
|
||||||
|
images.images.notify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.click()
|
input.click()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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"
|
||||||
@ -8,7 +7,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: CardTypes.NOTES,
|
type: "note",
|
||||||
title: "New Note",
|
title: "New Note",
|
||||||
contents: "",
|
contents: "",
|
||||||
position: {
|
position: {
|
||||||
@ -20,7 +19,7 @@ export function StickyNoteButton() {
|
|||||||
h: 200
|
h: 200
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
updateLocalStorage(CardTypes.NOTES, notes.notes)
|
updateLocalStorage("notes", notes.notes.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,14 +3,13 @@ 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: CardTypes.TEXTS,
|
type: "texts",
|
||||||
title: "New Note",
|
title: "New Note",
|
||||||
contents: "todo: fill me",
|
contents: "todo: fill me",
|
||||||
position: {
|
position: {
|
||||||
@ -22,7 +21,7 @@ export function TextButton() {
|
|||||||
h: 100
|
h: 100
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
updateLocalStorage(CardTypes.TEXTS, texts.texts).notify()
|
updateLocalStorage("texts", texts.texts.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
interface ExportIconProps {
|
namespace ExportIcon {
|
||||||
className?: string
|
export interface Props {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
export function ExportIcon({ className }: ExportIcon.Props) {
|
||||||
export function ExportIcon({ className }: ExportIconProps) {
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
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>)
|
|
||||||
}
|
|
@ -1 +1,4 @@
|
|||||||
|
export * from "./CanvasItem"
|
||||||
|
export * from "./CanvasControls"
|
||||||
export * from "./InfinateCanvas"
|
export * from "./InfinateCanvas"
|
||||||
|
export * from "./CardSelector"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { signal } from "kaioken"
|
import { signal } from "kaioken"
|
||||||
import { Card, CardTypes } from "../types"
|
import { Card } from "../types"
|
||||||
import { focusedItem } from "."
|
import { focusedItem } from "."
|
||||||
|
|
||||||
export type ImageCardType = Card<CardTypes.IMAGES>
|
export type ImageCardType = Card<"images">
|
||||||
|
|
||||||
const images = signal<Record<ImageCardType["id"], ImageCardType>>({})
|
const images = signal<Record<ImageCardType["id"], ImageCardType>>({})
|
||||||
|
|
||||||
@ -15,7 +15,6 @@ 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
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { signal } from "kaioken"
|
import { signal } from "kaioken"
|
||||||
import { Card, CardTypes } from "../types"
|
import { Card } from "../types"
|
||||||
import { focusedItem } from "."
|
import { focusedItem } from "."
|
||||||
|
|
||||||
export type NoteCardType = Card<CardTypes.NOTES>
|
export type NoteCardType = Card<"notes">
|
||||||
|
|
||||||
const notes = signal<Record<NoteCardType["id"], NoteCardType>>({})
|
const notes = signal<Record<NoteCardType["id"], NoteCardType>>({})
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { signal } from "kaioken"
|
import { signal } from "kaioken"
|
||||||
import { Card, CardTypes } from "../types"
|
import { Card } from "../types"
|
||||||
import { focusedItem } from "."
|
import { focusedItem } from "."
|
||||||
|
|
||||||
export type TextCardType = Card<CardTypes.TEXTS> & {
|
export type TextCardType = Card<"texts"> & {
|
||||||
fontSize: number
|
fontSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
|
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<TCard extends CardTypes> {
|
export interface Card<Ttype extends CardTypes> {
|
||||||
id: string
|
id: string
|
||||||
type: TCard
|
type: Ttype
|
||||||
title: string
|
title: string
|
||||||
contents: TCard extends "image" ? Base64 : string
|
contents: Ttype extends "image" ? Base64 : string
|
||||||
position: positionCoords
|
position: positionCoords
|
||||||
dimensions: dimensionCoords
|
dimensions: dimensionCoords
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
import { Signal } from "kaioken"
|
import { CardTypes } from "../types"
|
||||||
import { Card, CardTypes } from "../types"
|
|
||||||
|
|
||||||
export function updateLocalStorage(
|
export function updateLocalStorage(
|
||||||
location: CardTypes,
|
location: CardTypes,
|
||||||
collection: Signal<Record<string, Card<CardTypes>>>
|
collection: unknown[] | Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
try {
|
localStorage.setItem(location, JSON.stringify(collection))
|
||||||
localStorage.setItem(location, JSON.stringify(collection.value))
|
|
||||||
} catch (e) {
|
|
||||||
throw new DOMException(
|
|
||||||
"Could not update local storage " + e,
|
|
||||||
"LocalStorageError"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return collection
|
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@ import { sideEffectsEnabled, useHook } from "kaioken"
|
|||||||
import { noop } from "kaioken/utils"
|
import { noop } from "kaioken/utils"
|
||||||
|
|
||||||
type UseDebounceState = {
|
type UseDebounceState = {
|
||||||
timer: NodeJS.Timeout | undefined
|
timer: number
|
||||||
debounce: (func: (...args: unknown[]) => void, timeout?: number) => void
|
debounce: (this: any, func: Function, timeout?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function createState(): UseDebounceState {
|
function createState(): UseDebounceState {
|
||||||
return { timer: undefined, debounce: noop }
|
return { timer: 0, debounce: noop }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDebounce() {
|
export function useDebounce() {
|
||||||
@ -17,7 +17,8 @@ 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(
|
||||||
func: (...args: unknown[]) => void,
|
this: any,
|
||||||
|
func: Function,
|
||||||
timeout = 300
|
timeout = 300
|
||||||
) {
|
) {
|
||||||
clearTimeout(hook.timer)
|
clearTimeout(hook.timer)
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
@ -17,6 +15,8 @@
|
|||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve"
|
||||||
},
|
},
|
||||||
|
@ -1,25 +1,9 @@
|
|||||||
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: [
|
plugins: [kaioken()],
|
||||||
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
|
||||||
|
Loading…
Reference in New Issue
Block a user