Compare commits

...

13 Commits

27 changed files with 313 additions and 142 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"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@tauri-apps/cli": "^1",
"autoprefixer": "^10.4.18",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.11.0",
"postcss": "^8.4.35",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1",
"typescript": "^5.0.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.0.0",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-kaioken": "^0.13.1"
},
"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";
namespace ContextMenuPortal {
export interface Props {
interface ContextMenuPortalProps {
children: JSX.Children
open: boolean
closeAction: (() => void) | null | undefined
}
}
export function ContextMenuPortal({ children, open, closeAction }: ContextMenuPortal.Props) {
export function ContextMenuPortal({ children, open, closeAction }: ContextMenuPortalProps) {
const { mouse } = useMouse()
const pos = signal({ x: 0, y: 0 })
const ref = useRef<HTMLDivElement>(null)

View File

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

View File

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

View File

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

View File

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

View File

@ -12,14 +12,12 @@ import { createFileAndExport } from "../utils/createFileAndExport"
import { ContextMenuPortal } from "./ContextMenuPortal"
import { HelpIcon } from "./icons/HelpIcon"
namespace NoteCard {
export interface NoteCardProps {
interface NoteCardProps {
key: NoteCardType['id']
data: NoteCardType
}
}
export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
export function NoteCard({ key: itemKey, data: item }: NoteCardProps) {
const saved = signal(true)
const pressed = signal(false)
const newX = useRef(0)
@ -103,7 +101,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
saved.value = false
}
function _handleClose(_e: Event) {
function _handleClose() {
NotesSigal.default.removeNote(item.id)
NotesSigal.default.notes.notify()
updateLocalStorage()
@ -117,7 +115,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
createFileAndExport("Note", item.contents, "text/markdown")
}
function _handleExportClick(_e: MouseEvent) {
function _handleExportClick() {
_exportFile()
}
@ -149,11 +147,11 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
switch (e.key) {
case 'Delete':
e.preventDefault()
_handleClose(e)
_handleClose()
break
case 'Backspace':
e.preventDefault()
_handleClose(e)
_handleClose()
break
case 'e':
e.preventDefault()
@ -255,7 +253,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
{/* HOTKEY PAPER */}
{showHelp.value &&
<div
className={"text-white absolute bg-[#1c1c1c] rounded-md p-1 z-[1000] border border-blue-500"}
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`
@ -273,7 +271,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
}
function ExpandIcon({ cb }: {
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null | undefined
cb: ((this: GlobalEventHandlers, ev: MouseEvent) => void) | null | undefined
}) {
const isDarkTheme = useThemeDetector()

View File

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

View File

@ -8,6 +8,7 @@ import {
useEffect,
useState,
} from "kaioken"
import { noop } from "kaioken/utils"
type Toast = {
ts: number
@ -21,11 +22,15 @@ const defaultDuration = 3000
const ToastContext = createContext<{
showToast: (type: Toast["type"], message: string) => void
}>(null as any)
}>({ showToast: noop })
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[]>([])
useEffect(() => {

View File

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

View File

@ -1,44 +1,58 @@
import images from "../../signals/images"
import notes from "../../signals/notes"
import { Card } from "../../types"
import images, { ImageCardType } from "../../signals/images"
import notes, { NoteCardType } from "../../signals/notes"
import texts, { TextCardType } from "../../signals/texts"
import { Card, CardTypes } from "../../types"
import { convertBase64ToJson } from "../../utils/convertBase64ToJson"
import { updateLocalStorage } from "../../utils/localStorage"
import { Tooltip } from "./Tooltip"
import { defaultClassName } from "./utils"
type legacySupportTypes = { type: CardTypes | string }
export function ImportButton() {
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')
input.type = 'file'
input.accept = ".json"
input.multiple = false
input.onchange = (e: any) => {
const file = e.target.files[0]
input.onchange = (e: Event) => {
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()
reader.readAsDataURL(file)
reader.onload = function(readerEvent) {
let content = readerEvent.target?.result;
// get only the base64 parts and not any identifiers
content = (content as string).split(',')[1]
const data: Record<string, Card<'notes'> | Card<'images'>> = convertBase64ToJson(content)
for (let key in data) {
const data: Record<string, Omit<Card<CardTypes>, 'type'> & legacySupportTypes> = convertBase64ToJson(content)
for (const key in data) {
const item = data[key]
if (item.type == 'images') {
const { id, ...rest } = item
images.addImage(rest)
}
if (item.type == 'notes') {
const { id, ...rest } = item
notes.addNote(rest)
const { id: _, ...rest } = item
if (item.type === CardTypes.IMAGES || item.type == 'image') {
images.addImage(rest as ImageCardType)
continue
} else if (item.type === CardTypes.NOTES || item.type === 'note') {
notes.addNote(rest as NoteCardType)
continue
} else if (item.type === CardTypes.TEXTS || item.type === 'text') {
texts.addText(rest as TextCardType)
continue
}
}
updateLocalStorage('notes', notes.notes.value)
updateLocalStorage('images', images.images.value)
notes.notes.notify()
images.images.notify()
updateLocalStorage(CardTypes.NOTES, notes.notes).notify()
updateLocalStorage(CardTypes.IMAGES, images.images).notify()
updateLocalStorage(CardTypes.TEXTS, texts.texts).notify()
}
}
input.click()

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
namespace HelpIcon {
export interface Props {
interface HelpIconProps {
onMouseOver?: () => void
onMouseOut?: () => void
}
}
export function HelpIcon({ onMouseOver, onMouseOut }: HelpIcon.Props) {
export function HelpIcon({ onMouseOver, onMouseOut }: HelpIconProps) {
return (
<svg
@ -19,7 +18,7 @@ export function HelpIcon({ onMouseOver, onMouseOut }: HelpIcon.Props) {
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="w-4 h-4 hover:text-blue-500 cursor-pointer"
className="dark:text-[#5c5c5c] cursor-pointer w-4 h-4 text-[#9c9c9c] hover:text-blue-500 transition-color duration-300"
>
<circle
cx="12"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,17 @@
import { CardTypes } from "../types"
import { Signal } from "kaioken"
import { Card, CardTypes } from "../types"
export function updateLocalStorage(
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"
type UseDebounceState = {
timer: number
debounce: (this: any, func: Function, timeout?: number) => void
timer: NodeJS.Timeout | undefined
debounce: (func: (...args: unknown[]) => void, timeout?: number) => void
}
function createState(): UseDebounceState {
return { timer: 0, debounce: noop }
return { timer: undefined, debounce: noop }
}
export function useDebounce() {
@ -17,8 +17,7 @@ export function useDebounce() {
if (!isInit) return { timer: hook.timer, debounce: hook.debounce }
hook.debounce = function debounce(
this: any,
func: Function,
func: (...args: unknown[]) => void,
timeout = 300
) {
clearTimeout(hook.timer)

View File

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

View File

@ -1,9 +1,25 @@
import { defineConfig } from "vite"
import kaioken from "vite-plugin-kaioken"
import eslint from "vite-plugin-eslint"
import checker from "vite-plugin-checker"
// https://vitejs.dev/config/
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`
//
// 1. prevent vite from obscuring rust errors