Compare commits
22 Commits
v0.1.1
...
059329c696
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059329c696 | ||
|
|
5c826e6b93 | ||
|
|
6f76c46299 | ||
|
|
4feb6a880c | ||
|
|
45a6952c60 | ||
|
|
85c6c44393 | ||
|
|
c93fc2cc58 | ||
|
|
93edf204ce | ||
|
|
4d04f4f2ff | ||
|
|
99e6761e92 | ||
|
|
a1b339f668 | ||
|
|
efc9c73751 | ||
|
|
77b8ad2dcd | ||
|
|
23054d4981 | ||
|
|
a3bb474399 | ||
|
|
e48779e8e0 | ||
|
|
73349444d6 | ||
|
|
e9e0abe380 | ||
|
|
e843b7662d | ||
|
|
cc3ba79df0 | ||
|
|
2609d73bbd | ||
|
|
1c753a6f8f |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -35,6 +35,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Run platform build
|
- name: Run platform build
|
||||||
run: npm run ${{ matrix.script }}
|
run: npm run ${{ matrix.script }}
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
out
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
|
||||||
}
|
|
||||||
3
bin/generateEncryptionKey.js
Normal file
3
bin/generateEncryptionKey.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
console.log(crypto.randomBytes(32).toString('hex'))
|
||||||
@@ -24,9 +24,7 @@ mac:
|
|||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
notarize: true
|
||||||
cscLink: ${{ secrets.CSC_LINK }}
|
|
||||||
cscKeyPassword: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}.${ext}
|
||||||
linux:
|
linux:
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import filesystemPlugin from "@takerofnotes/plugin-filesystem";
|
|||||||
import supabasePlugin from "@takerofnotes/plugin-supabase";
|
import supabasePlugin from "@takerofnotes/plugin-supabase";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import { Index } from "flexsearch";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import __cjs_mod__ from "node:module";
|
import __cjs_mod__ from "node:module";
|
||||||
const __filename = import.meta.filename;
|
const __filename = import.meta.filename;
|
||||||
const __dirname = import.meta.dirname;
|
const __dirname = import.meta.dirname;
|
||||||
@@ -25,16 +23,21 @@ class PluginRegistry {
|
|||||||
return this.plugins.get(id);
|
return this.plugins.get(id);
|
||||||
}
|
}
|
||||||
list() {
|
list() {
|
||||||
return Array.from(this.plugins.values());
|
return Array.from(this.plugins.values()).map((plugin) => ({
|
||||||
|
id: plugin.id,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
configSchema: plugin.configSchema
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const USER_DATA_STRING = "__DEFAULT_USER_DATA__";
|
const USER_DATA_STRING = "__DEFAULT_USER_DATA__";
|
||||||
class PluginConfig {
|
class Config {
|
||||||
constructor(defaultPlugin) {
|
constructor(defaultPlugin) {
|
||||||
this.defaultPlugin = defaultPlugin;
|
this.defaultPlugin = defaultPlugin;
|
||||||
this.configPath = path.join(app.getPath("userData"), "config.json");
|
this.configPath = path.join(app.getPath("userData"), "config.json");
|
||||||
}
|
}
|
||||||
// Helper to replace placeholders with dynamic values, recursively
|
// Helper to replace placeholders with dynamic values
|
||||||
_resolveDefaults(config) {
|
_resolveDefaults(config) {
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
return config.map((item) => this._resolveDefaults(item));
|
return config.map((item) => this._resolveDefaults(item));
|
||||||
@@ -64,12 +67,15 @@ class PluginConfig {
|
|||||||
defaultConfig[field.key] = field.default ?? null;
|
defaultConfig[field.key] = field.default ?? null;
|
||||||
}
|
}
|
||||||
parsed = {
|
parsed = {
|
||||||
|
...parsed ? parsed : {},
|
||||||
activeAdapter: this.defaultPlugin.id,
|
activeAdapter: this.defaultPlugin.id,
|
||||||
adapterConfig: defaultConfig
|
adapters: {}
|
||||||
};
|
};
|
||||||
|
parsed.adapters[this.defaultPlugin.id] = defaultConfig;
|
||||||
|
parsed[theme] = "dark";
|
||||||
await this.write(parsed);
|
await this.write(parsed);
|
||||||
} else {
|
} else {
|
||||||
parsed.adapterConfig = this._resolveDefaults(parsed.adapterConfig);
|
parsed.adapters = this._resolveDefaults(parsed.adapters);
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@@ -78,7 +84,7 @@ class PluginConfig {
|
|||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
const resolvedConfig = {
|
const resolvedConfig = {
|
||||||
...configObject,
|
...configObject,
|
||||||
adapterConfig: this._resolveDefaults(configObject.adapterConfig)
|
adapters: this._resolveDefaults(configObject.adapters)
|
||||||
};
|
};
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
this.configPath,
|
this.configPath,
|
||||||
@@ -87,105 +93,14 @@ class PluginConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class NotesAPI {
|
const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 };
|
||||||
constructor(adapter) {
|
const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 };
|
||||||
if (!adapter) {
|
|
||||||
throw new Error("NotesAPI requires a storage adapter");
|
|
||||||
}
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.notesCache = /* @__PURE__ */ new Map();
|
|
||||||
this.index = new Index({
|
|
||||||
tokenize: "tolerant",
|
|
||||||
resolution: 9
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async init() {
|
|
||||||
await this.adapter.init();
|
|
||||||
const notes = await this.adapter.getAll();
|
|
||||||
for (const note of notes) {
|
|
||||||
this.notesCache.set(note.id, note);
|
|
||||||
this.index.add(note.id, note.title + "\n" + note.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* -----------------------
|
|
||||||
Public API
|
|
||||||
------------------------*/
|
|
||||||
getCategories() {
|
|
||||||
const categories = /* @__PURE__ */ new Set();
|
|
||||||
for (const note of this.notesCache.values()) {
|
|
||||||
if (note.category) {
|
|
||||||
categories.add(note.category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(categories).sort();
|
|
||||||
}
|
|
||||||
getCategoryNotes(categoryName) {
|
|
||||||
return Array.from(this.notesCache.values()).filter((n) => n.category === categoryName).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
||||||
}
|
|
||||||
getNote(id) {
|
|
||||||
return this.notesCache.get(id) ?? null;
|
|
||||||
}
|
|
||||||
async createNote(metadata = {}, content = "") {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
const note = {
|
|
||||||
id,
|
|
||||||
title: metadata.title || "Untitled",
|
|
||||||
category: metadata.category || null,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
content
|
|
||||||
};
|
|
||||||
this.notesCache.set(id, note);
|
|
||||||
this.index.add(id, note.title + "\n" + content);
|
|
||||||
await this.adapter.create(note);
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
async deleteNote(id) {
|
|
||||||
await this.adapter.delete(id);
|
|
||||||
this.notesCache.delete(id);
|
|
||||||
this.index.remove(id);
|
|
||||||
}
|
|
||||||
async updateNote(id, content) {
|
|
||||||
const note = this.notesCache.get(id);
|
|
||||||
if (!note) throw new Error("Note not found");
|
|
||||||
note.content = content;
|
|
||||||
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
this.index.update(id, note.title + "\n" + content);
|
|
||||||
await this.adapter.update(note);
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
async updateNoteMetadata(id, updates = {}) {
|
|
||||||
const note = this.notesCache.get(id);
|
|
||||||
if (!note) throw new Error("Note not found");
|
|
||||||
const allowedFields = ["title", "category"];
|
|
||||||
for (const key of Object.keys(updates)) {
|
|
||||||
if (!allowedFields.includes(key)) {
|
|
||||||
throw new Error(`Invalid metadata field: ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updates.title !== void 0) {
|
|
||||||
note.title = updates.title;
|
|
||||||
}
|
|
||||||
if (updates.category !== void 0) {
|
|
||||||
note.category = updates.category;
|
|
||||||
}
|
|
||||||
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
this.index.update(id, note.title + "\n" + note.content);
|
|
||||||
await this.adapter.update(note);
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
search(query) {
|
|
||||||
const ids = this.index.search(query);
|
|
||||||
return ids.map((id) => this.notesCache.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const preloadPath = join(__dirname, "../preload/index.mjs");
|
const preloadPath = join(__dirname, "../preload/index.mjs");
|
||||||
const rendererPath = join(__dirname, "../renderer/index.html");
|
const rendererPath = join(__dirname, "../renderer/index.html");
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const mainWindow2 = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 354,
|
width: DEFAULT_WINDOW_SIZE.width,
|
||||||
height: 549,
|
height: DEFAULT_WINDOW_SIZE.height,
|
||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@@ -193,23 +108,23 @@ function createWindow() {
|
|||||||
sandbox: false
|
sandbox: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mainWindow2.on("ready-to-show", () => {
|
mainWindow.on("ready-to-show", () => {
|
||||||
mainWindow2.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
mainWindow2.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
mainWindow2.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
||||||
} else {
|
} else {
|
||||||
mainWindow2.loadFile(rendererPath);
|
mainWindow.loadFile(rendererPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createNoteWindow(noteId) {
|
function createNoteWindow(noteId) {
|
||||||
const noteWindow = new BrowserWindow({
|
const noteWindow = new BrowserWindow({
|
||||||
width: 354,
|
width: DEFAULT_WINDOW_SIZE.width,
|
||||||
height: 549,
|
height: DEFAULT_WINDOW_SIZE.height,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: preloadPath,
|
preload: preloadPath,
|
||||||
@@ -220,11 +135,11 @@ function createNoteWindow(noteId) {
|
|||||||
});
|
});
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
noteWindow.loadURL(
|
noteWindow.loadURL(
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}/note/${noteId}`
|
`${process.env["ELECTRON_RENDERER_URL"]}/#/note/${noteId}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(rendererPath, {
|
noteWindow.loadFile(rendererPath, {
|
||||||
path: `/notes/${noteId}`
|
hash: `/note/${noteId}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,22 +147,68 @@ app.whenReady().then(async () => {
|
|||||||
ipcMain.on("open-note-window", (_, noteId) => {
|
ipcMain.on("open-note-window", (_, noteId) => {
|
||||||
createNoteWindow(noteId);
|
createNoteWindow(noteId);
|
||||||
});
|
});
|
||||||
|
const broadcastNoteChange = (event, data) => {
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => {
|
||||||
|
win.webContents.send(event, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
const registry = new PluginRegistry();
|
const registry = new PluginRegistry();
|
||||||
registry.register(filesystemPlugin);
|
registry.register(filesystemPlugin);
|
||||||
registry.register(supabasePlugin);
|
registry.register(supabasePlugin);
|
||||||
await new PluginConfig(filesystemPlugin).load();
|
const config = new Config(filesystemPlugin);
|
||||||
const plugin = registry.get(supabasePlugin.id);
|
const initialConfig = await config.load();
|
||||||
const adapter = plugin.createAdapter({
|
const setActivePlugin = async (pluginId) => {
|
||||||
supabaseKey: process.env.SUPABASE_KEY,
|
const currentConfig = await config.load();
|
||||||
supabaseUrl: process.env.SUPABASE_URL
|
await config.write({ ...currentConfig, activeAdapter: pluginId });
|
||||||
});
|
const plugin = registry.get(pluginId);
|
||||||
const notesAPI = new NotesAPI(adapter);
|
const adapterConfig = currentConfig.adapters[pluginId] || {};
|
||||||
await notesAPI.init();
|
const adapter = plugin.createAdapter(adapterConfig);
|
||||||
ipcMain.handle("notesAPI:call", (_, method, args) => {
|
await adapter.init();
|
||||||
if (!notesAPI[method]) {
|
ipcMain.removeHandler("adapter:call");
|
||||||
throw new Error("Invalid method");
|
ipcMain.handle("adapter:call", async (_, method, args) => {
|
||||||
|
if (!adapter[method]) {
|
||||||
|
throw new Error(`Invalid adapter method: ${method}`);
|
||||||
|
}
|
||||||
|
return await adapter[method](...args);
|
||||||
|
});
|
||||||
|
broadcastNoteChange("plugin-changed", pluginId);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
await setActivePlugin(initialConfig.activeAdapter);
|
||||||
|
ipcMain.handle("getConfig", async () => {
|
||||||
|
return await config.load();
|
||||||
|
});
|
||||||
|
ipcMain.handle("setConfig", async (_, newConfig) => {
|
||||||
|
await config.write(newConfig);
|
||||||
|
});
|
||||||
|
ipcMain.handle("listPlugins", async () => {
|
||||||
|
return registry.list();
|
||||||
|
});
|
||||||
|
ipcMain.handle("setActivePlugin", async (_, pluginId) => {
|
||||||
|
return await setActivePlugin(pluginId);
|
||||||
|
});
|
||||||
|
ipcMain.on("note-changed", (_, event, data) => {
|
||||||
|
broadcastNoteChange(event, data);
|
||||||
|
});
|
||||||
|
ipcMain.handle("move-opened", (_) => {
|
||||||
|
const activeWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
const windowSize = activeWindow.getSize();
|
||||||
|
if (windowSize[0] < DEFAULT_MOVE_WINDOW_SIZE.width) {
|
||||||
|
activeWindow.setSize(
|
||||||
|
DEFAULT_MOVE_WINDOW_SIZE.width,
|
||||||
|
DEFAULT_MOVE_WINDOW_SIZE.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle("move-closed", (_) => {
|
||||||
|
const activeWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
const windowSize = activeWindow.getSize();
|
||||||
|
if (windowSize[0] === 708) {
|
||||||
|
activeWindow.setSize(
|
||||||
|
DEFAULT_WINDOW_SIZE.width,
|
||||||
|
DEFAULT_WINDOW_SIZE.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return notesAPI[method](...args);
|
|
||||||
});
|
});
|
||||||
electronApp.setAppUserModelId("com.electron");
|
electronApp.setAppUserModelId("com.electron");
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
|||||||
@@ -1,19 +1,45 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
const api = {
|
const api = {
|
||||||
|
getConfig: () => ipcRenderer.invoke("getConfig"),
|
||||||
|
setConfig: (config) => ipcRenderer.invoke("setConfig", config),
|
||||||
|
listPlugins: () => ipcRenderer.invoke("listPlugins"),
|
||||||
|
setActivePlugin: (pluginId) => ipcRenderer.invoke("setActivePlugin", pluginId),
|
||||||
openNoteWindow: (noteId) => {
|
openNoteWindow: (noteId) => {
|
||||||
ipcRenderer.send("open-note-window", noteId);
|
ipcRenderer.send("open-note-window", noteId);
|
||||||
|
},
|
||||||
|
onNoteCreated: (callback) => {
|
||||||
|
ipcRenderer.on("note-created", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
onNoteUpdated: (callback) => {
|
||||||
|
ipcRenderer.on("note-updated", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
onNoteDeleted: (callback) => {
|
||||||
|
ipcRenderer.on("note-deleted", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
onPluginChanged: (callback) => {
|
||||||
|
ipcRenderer.on("plugin-changed", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
notifyNoteChanged: (event, data) => {
|
||||||
|
ipcRenderer.send("note-changed", event, data);
|
||||||
|
},
|
||||||
|
moveOpened: () => {
|
||||||
|
ipcRenderer.invoke("move-opened");
|
||||||
|
},
|
||||||
|
moveClosed: () => {
|
||||||
|
ipcRenderer.invoke("move-closed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const notesAPI = {
|
const adapter = {
|
||||||
call: (method, ...args) => ipcRenderer.invoke("notesAPI:call", method, args)
|
call: (method, ...args) => ipcRenderer.invoke("adapter:call", method, args)
|
||||||
};
|
};
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld("api", api);
|
contextBridge.exposeInMainWorld("api", api);
|
||||||
contextBridge.exposeInMainWorld("notesAPI", notesAPI);
|
contextBridge.exposeInMainWorld("adapter", adapter);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.api = api;
|
window.api = api;
|
||||||
|
window.adapter = adapter;
|
||||||
}
|
}
|
||||||
|
|||||||
4
out/renderer/assets/__vite-browser-external-2Ng8QIWW.js
Normal file
4
out/renderer/assets/__vite-browser-external-2Ng8QIWW.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const __viteBrowserExternal = {};
|
||||||
|
export {
|
||||||
|
__viteBrowserExternal as default
|
||||||
|
};
|
||||||
@@ -1,897 +0,0 @@
|
|||||||
@charset "UTF-8";
|
|
||||||
/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:root {
|
|
||||||
--black: #181818;
|
|
||||||
--white: #D5D5D5;
|
|
||||||
--grey-100: #747474;
|
|
||||||
--green: #87FF5B;
|
|
||||||
--blue: #5B92FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root .theme-light {
|
|
||||||
--theme-bg: #D5D5D5;
|
|
||||||
--theme-fg: #181818;
|
|
||||||
--theme-accent: #87FF5B;
|
|
||||||
--theme-link: #5B92FF;
|
|
||||||
}
|
|
||||||
:root .theme-dark {
|
|
||||||
--theme-bg: #181818;
|
|
||||||
--theme-fg: #D5D5D5;
|
|
||||||
--theme-accent: #87FF5B;
|
|
||||||
--theme-link: #5B92FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
|
|
||||||
--ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
|
||||||
--ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22);
|
|
||||||
--ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
|
||||||
--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
|
||||||
--ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335);
|
|
||||||
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
--ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
||||||
--ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
|
|
||||||
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
|
||||||
--ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1);
|
|
||||||
--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
|
||||||
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
||||||
--ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1);
|
|
||||||
--ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
|
|
||||||
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
|
|
||||||
--ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
|
||||||
--ease-custom: cubic-bezier(0.315, 0.365, 0.23, 0.985);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
The new CSS reset - version 1.9 (last updated 19.6.2023)
|
|
||||||
GitHub page: https://github.com/elad2412/the-new-css-reset
|
|
||||||
***/
|
|
||||||
/*
|
|
||||||
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
|
||||||
- The "symbol *" part is to solve Firefox SVG sprite bug
|
|
||||||
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
|
|
||||||
*/
|
|
||||||
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
|
|
||||||
all: unset;
|
|
||||||
display: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preferred box-sizing value */
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reapply the pointer cursor for anchor tags */
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
cursor: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For images to not be able to exceed their container */
|
|
||||||
img {
|
|
||||||
max-inline-size: 100%;
|
|
||||||
max-block-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* removes spacing between cells in tables */
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
-webkit-user-select: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* revert the 'white-space' property for textarea elements on Safari */
|
|
||||||
textarea {
|
|
||||||
white-space: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* minimum style to allow to style meter element */
|
|
||||||
meter {
|
|
||||||
-webkit-appearance: revert;
|
|
||||||
appearance: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* preformatted text - use only for this feature */
|
|
||||||
:where(pre) {
|
|
||||||
all: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* reset default text opacity of input placeholder */
|
|
||||||
::placeholder {
|
|
||||||
color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* remove default dot (•) sign */
|
|
||||||
::marker {
|
|
||||||
content: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fix the feature of 'hidden' attribute.
|
|
||||||
display:revert; revert to element instead of attribute */
|
|
||||||
:where([hidden]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* revert for bug in Chromium browsers
|
|
||||||
- fix for the content editable attribute will work properly.
|
|
||||||
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
|
||||||
:where([contenteditable]:not([contenteditable=false])) {
|
|
||||||
-moz-user-modify: read-write;
|
|
||||||
-webkit-user-modify: read-write;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
-webkit-line-break: after-white-space;
|
|
||||||
-webkit-user-select: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* apply back the draggable feature - exist only in Chromium and Safari */
|
|
||||||
:where([draggable=true]) {
|
|
||||||
-webkit-user-drag: element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Revert Modal native behavior */
|
|
||||||
:where(dialog:modal) {
|
|
||||||
all: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lily-cursor {
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
|
||||||
width: 100vw;
|
|
||||||
position: relative;
|
|
||||||
left: 50%;
|
|
||||||
right: 50%;
|
|
||||||
margin-left: -50vw;
|
|
||||||
margin-right: -50vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-hidden {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.mobile-only {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 799px) {
|
|
||||||
.desktop-only {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.has-scroll-smooth) .hide-on-native-scroll {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.has-scroll-smooth .hide-on-smooth-scroll {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Font Weights:
|
|
||||||
100 - Thin
|
|
||||||
200 - Extra Light (Ultra Light)
|
|
||||||
300 - Light
|
|
||||||
400 - Normal
|
|
||||||
500 - Medium
|
|
||||||
600 - Semi Bold (Demi Bold)
|
|
||||||
700 - Bold
|
|
||||||
800 - Extra Bold (Ultra Bold)
|
|
||||||
900 - Black (Heavy)
|
|
||||||
*/
|
|
||||||
/* Leibniz Fraktur */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Leibniz Fraktur";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url("./leibniz-fraktur-BqdMSuEI.woff2") format("woff2"), url("./leibniz-fraktur-CvnkUeBX.woff") format("woff");
|
|
||||||
}
|
|
||||||
/* Geist Mono */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Geist Mono";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url("./geist-mono-bold-CTLtpKvJ.woff2") format("woff2"), url("./geist-mono-bold-Bz_UliG4.woff") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Geist Mono";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url("./geist-mono-BzrJhchg.woff2") format("woff2"), url("./geist-mono-OFKGen7b.woff") format("woff");
|
|
||||||
}
|
|
||||||
:root {
|
|
||||||
--font-display: 'Leibniz Fraktur', serif;
|
|
||||||
--font-mono: 'Geist Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--layout-column-count: 5;
|
|
||||||
--layout-column-gap: 5.6497175141vw;
|
|
||||||
--layout-margin: 8.4745762712vw;
|
|
||||||
--layout-width: calc(100vw - (2 * var(--layout-margin)));
|
|
||||||
--layout-column-width: calc(
|
|
||||||
(
|
|
||||||
var(--layout-width) -
|
|
||||||
(
|
|
||||||
(var(--layout-column-count) - 1) *
|
|
||||||
var(--layout-column-gap)
|
|
||||||
)
|
|
||||||
) /
|
|
||||||
var(--layout-column-count)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-block, .layout-grid {
|
|
||||||
max-width: var(--layout-width);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-block-inner, .layout-grid-inner {
|
|
||||||
padding-left: var(--layout-margin);
|
|
||||||
padding-right: var(--layout-margin);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
|
||||||
grid-gap: var(--layout-column-gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-grid-inner {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
|
||||||
grid-gap: var(--layout-column-gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.dev),
|
|
||||||
html:not(.dev) * {
|
|
||||||
scrollbar-width: none !important;
|
|
||||||
-ms-overflow-style: none !important;
|
|
||||||
}
|
|
||||||
html:not(.dev)::-webkit-scrollbar,
|
|
||||||
html:not(.dev) *::-webkit-scrollbar {
|
|
||||||
width: 0 !important;
|
|
||||||
height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.lenis {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-smooth {
|
|
||||||
scroll-behavior: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-stopped {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-scrolling iframe {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 400ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-fade-enter-active,
|
|
||||||
.quick-fade-leave-active {
|
|
||||||
transition: opacity 100ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-fade-enter-from,
|
|
||||||
.quick-fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slow-fade-enter-active,
|
|
||||||
.slow-fade-leave-active {
|
|
||||||
transition: opacity 600ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slow-fade-enter-from,
|
|
||||||
.slow-fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-up-enter-active,
|
|
||||||
.slide-up-leave-active {
|
|
||||||
transition: transform 400ms var(--ease-out-quad);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-up-enter-from,
|
|
||||||
.slide-up-leave-to {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-left-enter-active,
|
|
||||||
.slide-left-leave-active {
|
|
||||||
transition: transform 400ms var(--ease-out-quad);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-left-enter-from,
|
|
||||||
.slide-left-leave-to {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
background: var(--black);
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h1,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
line-height: 1.3;
|
|
||||||
font-size: 8.4745762712vw;
|
|
||||||
}
|
|
||||||
.h1.mono,
|
|
||||||
h1.mono,
|
|
||||||
h2.mono,
|
|
||||||
h3.mono,
|
|
||||||
h4.mono,
|
|
||||||
h5.mono,
|
|
||||||
h6.mono {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 6.2146892655vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p,
|
|
||||||
p,
|
|
||||||
a,
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
pre {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
color: var(--theme-bg);
|
|
||||||
background: var(--theme-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
|
||||||
color: var(--theme-bg);
|
|
||||||
background: var(--theme-accent);
|
|
||||||
}/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
min-height: calc(100 * var(--vh));
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: clip;
|
|
||||||
background: var(--theme-bg);
|
|
||||||
color: var(--theme-fg);
|
|
||||||
transition: opacity 1000ms;
|
|
||||||
}
|
|
||||||
.container:not(.fonts-ready) {
|
|
||||||
opacity: 0;
|
|
||||||
}/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.category-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 7.3446327684vw 1fr;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
padding: 1.4124293785vw 0 4.2372881356vw;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.category-row .index {
|
|
||||||
margin-top: 5.3672316384vw;
|
|
||||||
}
|
|
||||||
.category-row .title {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
display: -webkit-box;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
@media (max-width: 799px) {
|
|
||||||
.category-row .title {
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.category-row::after {
|
|
||||||
content: "----------------------------------------";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.category-row.router-link-exact-active {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.category-row:hover:not(.router-link-exact-active) {
|
|
||||||
color: var(--theme-accent);
|
|
||||||
}/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.note-row {
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
display: grid;
|
|
||||||
gap: 5.6497175141vw;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.note-row .title {
|
|
||||||
width: 44.9152542373vw;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.note-row .title::after {
|
|
||||||
content: "(open)";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
font-weight: 700;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.note-row:hover {
|
|
||||||
color: var(--theme-accent);
|
|
||||||
}
|
|
||||||
.note-row:hover .title::after {
|
|
||||||
opacity: 1;
|
|
||||||
}/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.directory {
|
|
||||||
padding-top: 5.0847457627vw;
|
|
||||||
}
|
|
||||||
.directory .label {
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 4.802259887vw 0 6.7796610169vw;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
}
|
|
||||||
.directory .notes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3.9548022599vw;
|
|
||||||
}/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.editor {
|
|
||||||
padding-top: 2.2598870056vw;
|
|
||||||
padding-bottom: 5.6497175141vw;
|
|
||||||
}
|
|
||||||
.editor h1 {
|
|
||||||
font-weight: 700 !important;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
}
|
|
||||||
.editor h1:first-child {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
font-weight: 400 !important;
|
|
||||||
}
|
|
||||||
.editor h1:first-child:first-child::first-letter {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 11.8644067797vw;
|
|
||||||
}
|
|
||||||
.editor h1.is-editor-empty:first-child::before {
|
|
||||||
color: var(--grey-100);
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
pointer-events: none;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
font-weight: 400 !important;
|
|
||||||
}
|
|
||||||
.editor h1.is-editor-empty:first-child::before:first-child::first-letter {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 11.8644067797vw;
|
|
||||||
}
|
|
||||||
.editor p strong {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.editor p em {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.editor hr::before {
|
|
||||||
content: "----------------------------------------";
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
}
|
|
||||||
.editor ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
.editor ul li {
|
|
||||||
display: list-item;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
.editor ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
}
|
|
||||||
.editor ol li {
|
|
||||||
display: list-item;
|
|
||||||
margin-left: 1.5em;
|
|
||||||
}
|
|
||||||
.editor ol li::marker {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 3.3898305085vw;
|
|
||||||
}
|
|
||||||
.editor a {
|
|
||||||
color: var(--theme-link);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.editor .editor-wrap > div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5.6497175141vw;
|
|
||||||
}
|
|
||||||
.editor .editor-wrap > div:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.editor .bubble-menu {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.4124293785vw;
|
|
||||||
border: 1px solid var(--grey-100);
|
|
||||||
color: var(--grey-100);
|
|
||||||
border-radius: 0.2em;
|
|
||||||
background: var(--theme-bg);
|
|
||||||
}
|
|
||||||
.editor .bubble-menu button {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.2em;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
}
|
|
||||||
.editor .bubble-menu button:hover {
|
|
||||||
background: var(--grey-100);
|
|
||||||
color: var(--theme-bg);
|
|
||||||
}
|
|
||||||
.editor .bubble-menu button.active {
|
|
||||||
background: var(--theme-fg);
|
|
||||||
color: var(--theme-bg);
|
|
||||||
}/* Breakpoints */
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.category .back {
|
|
||||||
display: block;
|
|
||||||
opacity: 0.25;
|
|
||||||
margin-top: 2.5423728814vw;
|
|
||||||
}
|
|
||||||
.category .category-row {
|
|
||||||
margin-top: 1.1299435028vw;
|
|
||||||
}
|
|
||||||
.category .notes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3.9548022599vw;
|
|
||||||
margin-top: 2.5423728814vw;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
BIN
out/renderer/assets/neuefraktur-A4S1ACH2.woff2
Normal file
BIN
out/renderer/assets/neuefraktur-A4S1ACH2.woff2
Normal file
Binary file not shown.
BIN
out/renderer/assets/neuefraktur-CwjUIZ0G.woff
Normal file
BIN
out/renderer/assets/neuefraktur-CwjUIZ0G.woff
Normal file
Binary file not shown.
@@ -8,8 +8,8 @@
|
|||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||||
/>
|
/>
|
||||||
<script type="module" crossorigin src="./assets/index-Dv1qfmwr.js"></script>
|
<script type="module" crossorigin src="./assets/index-D2TWwJ08.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-CZWw79gc.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BFwBEQYI.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
337
package-lock.json
generated
337
package-lock.json
generated
@@ -1,23 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "app.takerofnotes.com",
|
"name": "takerofnotes-app",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app.takerofnotes.com",
|
"name": "takerofnotes-app",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@fuzzco/font-loader": "^1.0.2",
|
"@fuzzco/font-loader": "^1.0.2",
|
||||||
"@takerofnotes/plugin-filesystem": "^0.1.1",
|
"@takerofnotes/plugin-filesystem": "^0.2.0",
|
||||||
"@takerofnotes/plugin-supabase": "^0.1.0",
|
"@takerofnotes/plugin-supabase": "^0.1.0",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.20.0",
|
||||||
"@tiptap/extension-document": "^3.19.0",
|
"@tiptap/extension-document": "^3.19.0",
|
||||||
"@tiptap/extension-image": "^3.19.0",
|
"@tiptap/extension-highlight": "^3.20.0",
|
||||||
"@tiptap/extension-table": "^3.19.0",
|
"@tiptap/extension-list": "^3.20.0",
|
||||||
"@tiptap/markdown": "^3.19.0",
|
"@tiptap/markdown": "^3.20.4",
|
||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@tiptap/vue-3": "^3.19.0",
|
"@tiptap/vue-3": "^3.19.0",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
@@ -26,8 +27,10 @@
|
|||||||
"fecha": "^4.2.3",
|
"fecha": "^4.2.3",
|
||||||
"flexsearch": "^0.8.212",
|
"flexsearch": "^0.8.212",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"lenis": "^1.3.17",
|
"libsodium-wrappers": "^0.8.2",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
|
"markdown-it": "^14.1.1",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.97.3",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.97.3",
|
||||||
"tempus": "^1.0.0-dev.17",
|
"tempus": "^1.0.0-dev.17",
|
||||||
@@ -2343,19 +2346,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@takerofnotes/plugin-filesystem": {
|
"node_modules/@takerofnotes/plugin-filesystem": {
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.2.0.tgz",
|
||||||
"integrity": "sha512-u3L6HLxN/+t7PTtzzRA2uzgaVh/O1vasngU/tQeC6JsTnlIrypWFVbAlTS5zT9Km4y+8w6C16eEq7tcN8+5lPg==",
|
"integrity": "sha512-BP7HBN0SKAqBiv5pDtXpyVmkW9UrOnPXpKeThTwQTIShHyN5aPaD8ZEyv8+vfTSs3qnXWGplNdEPVjbQmc27+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@takerofnotes/plugin-sdk": "^0.1.0",
|
"@takerofnotes/plugin-sdk": "^0.3.1"
|
||||||
"gray-matter": "^4.0.3"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@takerofnotes/plugin-sdk": {
|
"node_modules/@takerofnotes/plugin-sdk": {
|
||||||
"version": "0.1.0",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.3.1.tgz",
|
||||||
"integrity": "sha512-ofhwwiQ59kNMEg2vvYoNq5JdXHB9/6TkDsbyroM5nsP/VPUvJSQ5g0UWCYBhteSzJ36iFUB+LtUwVt6gOXDClw==",
|
"integrity": "sha512-9GfPKyu1n52N00zYlLK32wdmGdc2uSd0jTj6UEixauW0TXn/7hD6SLpLRGSXcfZOJGGoi3iQk4MfjNsthe2ucw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
@@ -2381,16 +2383,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/core": {
|
"node_modules/@tiptap/core": {
|
||||||
"version": "3.20.0",
|
"version": "3.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
|
||||||
"integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==",
|
"integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/pm": "^3.20.0"
|
"@tiptap/pm": "^3.20.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-blockquote": {
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
@@ -2464,17 +2466,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-code-block": {
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
"version": "3.19.0",
|
"version": "3.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz",
|
||||||
"integrity": "sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg==",
|
"integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^3.19.0",
|
"@tiptap/core": "^3.20.0",
|
||||||
"@tiptap/pm": "^3.19.0"
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block-lowlight": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-9lN9rn07lOWkLnByT5C1axtq56MHpOI7MpLaCmX3p+x1bDl6Uvixm6AoBdTLfZUmUYeEFBsf7t5cR+QepMbkiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/extension-code-block": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0",
|
||||||
|
"highlight.js": "^11",
|
||||||
|
"lowlight": "^2 || ^3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-document": {
|
"node_modules/@tiptap/extension-document": {
|
||||||
@@ -2558,6 +2577,19 @@
|
|||||||
"@tiptap/core": "^3.19.0"
|
"@tiptap/core": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-highlight": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-ANL1wFz0s1ScNR4uBfO0s6Sz+qqGp2u6ynrCVk6TCT3d10CQ+gD1gSDTrVRC3CtlMKtHHH4fYrFAq284+J0gKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
|
||||||
@@ -2572,19 +2604,6 @@
|
|||||||
"@tiptap/pm": "^3.19.0"
|
"@tiptap/pm": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-image": {
|
|
||||||
"version": "3.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.19.0.tgz",
|
|
||||||
"integrity": "sha512-/rGl8nBziBPVJJ/9639eQWFDKcI3RQsDM3s+cqYQMFQfMqc7sQB9h4o4sHCBpmKxk3Y0FV/0NjnjLbBVm8OKdQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tiptap/core": "^3.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tiptap/extension-italic": {
|
"node_modules/@tiptap/extension-italic": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
|
||||||
@@ -2694,20 +2713,6 @@
|
|||||||
"@tiptap/core": "^3.19.0"
|
"@tiptap/core": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-table": {
|
|
||||||
"version": "3.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.19.0.tgz",
|
|
||||||
"integrity": "sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tiptap/core": "^3.19.0",
|
|
||||||
"@tiptap/pm": "^3.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tiptap/extension-text": {
|
"node_modules/@tiptap/extension-text": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
|
||||||
@@ -2749,9 +2754,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/markdown": {
|
"node_modules/@tiptap/markdown": {
|
||||||
"version": "3.19.0",
|
"version": "3.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.20.4.tgz",
|
||||||
"integrity": "sha512-Pnfacq2FHky1rqwmGwEmUJxuZu8VZ8XjaJIqsQC34S3CQWiOU+PukC9In2odzcooiVncLWT9s97jKuYpbmF1tQ==",
|
"integrity": "sha512-1ARtZzJ1skQCZi4LyVSmImgg6JIIMP5dEs0FvHXS3a7M3O+uMOUvY1sWeggVZExg8DXoVyHd7BjRIpm7uNRuSw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": "^17.0.1"
|
"marked": "^17.0.1"
|
||||||
@@ -2761,14 +2766,14 @@
|
|||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^3.19.0",
|
"@tiptap/core": "^3.20.4",
|
||||||
"@tiptap/pm": "^3.19.0"
|
"@tiptap/pm": "^3.20.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/pm": {
|
"node_modules/@tiptap/pm": {
|
||||||
"version": "3.20.0",
|
"version": "3.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
|
||||||
"integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==",
|
"integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-changeset": "^2.3.0",
|
"prosemirror-changeset": "^2.3.0",
|
||||||
@@ -2890,6 +2895,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hast": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/unist": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
@@ -2970,6 +2984,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/unist": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/verror": {
|
"node_modules/@types/verror": {
|
||||||
"version": "1.10.11",
|
"version": "1.10.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
||||||
@@ -4422,6 +4442,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -4439,6 +4468,19 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/devlop": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dir-compare": {
|
"node_modules/dir-compare": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
|
||||||
@@ -5105,19 +5147,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esprima": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"bin": {
|
|
||||||
"esparse": "bin/esparse.js",
|
|
||||||
"esvalidate": "bin/esvalidate.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
@@ -5137,18 +5166,6 @@
|
|||||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/extend-shallow": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-extendable": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -5625,49 +5642,6 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/gray-matter": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"js-yaml": "^3.13.1",
|
|
||||||
"kind-of": "^6.0.2",
|
|
||||||
"section-matter": "^1.0.0",
|
|
||||||
"strip-bom-string": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/argparse": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"sprintf-js": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/js-yaml": {
|
|
||||||
"version": "3.14.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
|
||||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^1.0.7",
|
|
||||||
"esprima": "^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"js-yaml": "bin/js-yaml.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/sprintf-js": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/gsap": {
|
"node_modules/gsap": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
||||||
@@ -5738,6 +5712,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
@@ -5930,15 +5913,6 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-extendable": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -6155,45 +6129,25 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/kind-of": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lazy-val": {
|
"node_modules/lazy-val": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lenis": {
|
"node_modules/libsodium": {
|
||||||
"version": "1.3.17",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.17.tgz",
|
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz",
|
||||||
"integrity": "sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==",
|
"integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==",
|
||||||
"license": "MIT",
|
"license": "ISC"
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/darkroomengineering"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"node_modules/libsodium-wrappers": {
|
||||||
"@nuxt/kit": ">=3.0.0",
|
"version": "0.8.2",
|
||||||
"react": ">=17.0.0",
|
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz",
|
||||||
"vue": ">=3.0.0"
|
"integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==",
|
||||||
},
|
"license": "ISC",
|
||||||
"peerDependenciesMeta": {
|
"dependencies": {
|
||||||
"@nuxt/kit": {
|
"libsodium": "^0.8.0"
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"vue": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/linkify-it": {
|
"node_modules/linkify-it": {
|
||||||
@@ -6273,6 +6227,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lowlight": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^3.0.0",
|
||||||
|
"devlop": "^1.0.0",
|
||||||
|
"highlight.js": "~11.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -6360,9 +6329,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
|
||||||
"integrity": "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA==",
|
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
@@ -8121,19 +8090,6 @@
|
|||||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/section-matter": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"extend-shallow": "^2.0.1",
|
|
||||||
"kind-of": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -8416,15 +8372,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-bom-string": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sumchecker": {
|
"node_modules/sumchecker": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "app.takerofnotes.com",
|
"name": "takerofnotes-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "An Electron application with Vue",
|
"description": "An Electron application with Vue",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -28,12 +28,13 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@fuzzco/font-loader": "^1.0.2",
|
"@fuzzco/font-loader": "^1.0.2",
|
||||||
"@takerofnotes/plugin-filesystem": "^0.1.1",
|
"@takerofnotes/plugin-filesystem": "^0.2.0",
|
||||||
"@takerofnotes/plugin-supabase": "^0.1.0",
|
"@takerofnotes/plugin-supabase": "^0.1.0",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.20.0",
|
||||||
"@tiptap/extension-document": "^3.19.0",
|
"@tiptap/extension-document": "^3.19.0",
|
||||||
"@tiptap/extension-image": "^3.19.0",
|
"@tiptap/extension-highlight": "^3.20.0",
|
||||||
"@tiptap/extension-table": "^3.19.0",
|
"@tiptap/extension-list": "^3.20.0",
|
||||||
"@tiptap/markdown": "^3.19.0",
|
"@tiptap/markdown": "^3.20.4",
|
||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@tiptap/vue-3": "^3.19.0",
|
"@tiptap/vue-3": "^3.19.0",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
@@ -42,8 +43,10 @@
|
|||||||
"fecha": "^4.2.3",
|
"fecha": "^4.2.3",
|
||||||
"flexsearch": "^0.8.212",
|
"flexsearch": "^0.8.212",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"lenis": "^1.3.17",
|
"libsodium-wrappers": "^0.8.2",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
|
"markdown-it": "^14.1.1",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.97.3",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.97.3",
|
||||||
"tempus": "^1.0.0-dev.17",
|
"tempus": "^1.0.0-dev.17",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
resources/fonts/neuefraktur.woff
Normal file
BIN
resources/fonts/neuefraktur.woff
Normal file
Binary file not shown.
BIN
resources/fonts/neuefraktur.woff2
Normal file
BIN
resources/fonts/neuefraktur.woff2
Normal file
Binary file not shown.
@@ -4,13 +4,13 @@ import { app } from 'electron'
|
|||||||
|
|
||||||
const USER_DATA_STRING = '__DEFAULT_USER_DATA__'
|
const USER_DATA_STRING = '__DEFAULT_USER_DATA__'
|
||||||
|
|
||||||
export default class PluginConfig {
|
export default class Config {
|
||||||
constructor(defaultPlugin) {
|
constructor(defaultPlugin) {
|
||||||
this.defaultPlugin = defaultPlugin
|
this.defaultPlugin = defaultPlugin
|
||||||
this.configPath = path.join(app.getPath('userData'), 'config.json')
|
this.configPath = path.join(app.getPath('userData'), 'config.json')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to replace placeholders with dynamic values, recursively
|
// Helper to replace placeholders with dynamic values
|
||||||
_resolveDefaults(config) {
|
_resolveDefaults(config) {
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
return config.map((item) => this._resolveDefaults(item))
|
return config.map((item) => this._resolveDefaults(item))
|
||||||
@@ -48,14 +48,18 @@ export default class PluginConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parsed = {
|
parsed = {
|
||||||
|
...(parsed ? parsed : {}),
|
||||||
activeAdapter: this.defaultPlugin.id,
|
activeAdapter: this.defaultPlugin.id,
|
||||||
adapterConfig: defaultConfig,
|
adapters: {},
|
||||||
}
|
}
|
||||||
|
parsed.adapters[this.defaultPlugin.id] = defaultConfig
|
||||||
|
|
||||||
|
parsed[theme] = 'dark'
|
||||||
|
|
||||||
await this.write(parsed)
|
await this.write(parsed)
|
||||||
} else {
|
} else {
|
||||||
// Ensure any "__DEFAULT_USER_DATA__" values are resolved on load
|
// Ensure any "__DEFAULT_USER_DATA__" values are resolved on load
|
||||||
parsed.adapterConfig = this._resolveDefaults(parsed.adapterConfig)
|
parsed.adapters = this._resolveDefaults(parsed.adapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
@@ -70,7 +74,7 @@ export default class PluginConfig {
|
|||||||
// Resolve defaults before writing
|
// Resolve defaults before writing
|
||||||
const resolvedConfig = {
|
const resolvedConfig = {
|
||||||
...configObject,
|
...configObject,
|
||||||
adapterConfig: this._resolveDefaults(configObject.adapterConfig),
|
adapters: this._resolveDefaults(configObject.adapters),
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { Index } from 'flexsearch'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
export default class NotesAPI {
|
|
||||||
constructor(adapter) {
|
|
||||||
if (!adapter) {
|
|
||||||
throw new Error('NotesAPI requires a storage adapter')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.adapter = adapter
|
|
||||||
this.notesCache = new Map()
|
|
||||||
|
|
||||||
this.index = new Index({
|
|
||||||
tokenize: 'tolerant',
|
|
||||||
resolution: 9,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.adapter.init()
|
|
||||||
|
|
||||||
const notes = await this.adapter.getAll()
|
|
||||||
|
|
||||||
for (const note of notes) {
|
|
||||||
this.notesCache.set(note.id, note)
|
|
||||||
this.index.add(note.id, note.title + '\n' + note.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------
|
|
||||||
Public API
|
|
||||||
------------------------*/
|
|
||||||
getCategories() {
|
|
||||||
const categories = new Set()
|
|
||||||
|
|
||||||
for (const note of this.notesCache.values()) {
|
|
||||||
if (note.category) {
|
|
||||||
categories.add(note.category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(categories).sort()
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategoryNotes(categoryName) {
|
|
||||||
return Array.from(this.notesCache.values())
|
|
||||||
.filter((n) => n.category === categoryName)
|
|
||||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
|
||||||
}
|
|
||||||
|
|
||||||
getNote(id) {
|
|
||||||
return this.notesCache.get(id) ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
async createNote(metadata = {}, content = '') {
|
|
||||||
const id = crypto.randomUUID()
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
|
|
||||||
const note = {
|
|
||||||
id,
|
|
||||||
title: metadata.title || 'Untitled',
|
|
||||||
category: metadata.category || null,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notesCache.set(id, note)
|
|
||||||
this.index.add(id, note.title + '\n' + content)
|
|
||||||
|
|
||||||
await this.adapter.create(note)
|
|
||||||
|
|
||||||
return note
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote(id) {
|
|
||||||
await this.adapter.delete(id)
|
|
||||||
|
|
||||||
this.notesCache.delete(id)
|
|
||||||
this.index.remove(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateNote(id, content) {
|
|
||||||
const note = this.notesCache.get(id)
|
|
||||||
if (!note) throw new Error('Note not found')
|
|
||||||
|
|
||||||
note.content = content
|
|
||||||
note.updatedAt = new Date().toISOString()
|
|
||||||
|
|
||||||
this.index.update(id, note.title + '\n' + content)
|
|
||||||
|
|
||||||
await this.adapter.update(note)
|
|
||||||
|
|
||||||
return note
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateNoteMetadata(id, updates = {}) {
|
|
||||||
const note = this.notesCache.get(id)
|
|
||||||
if (!note) throw new Error('Note not found')
|
|
||||||
|
|
||||||
const allowedFields = ['title', 'category']
|
|
||||||
for (const key of Object.keys(updates)) {
|
|
||||||
if (!allowedFields.includes(key)) {
|
|
||||||
throw new Error(`Invalid metadata field: ${key}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.title !== undefined) {
|
|
||||||
note.title = updates.title
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.category !== undefined) {
|
|
||||||
note.category = updates.category
|
|
||||||
}
|
|
||||||
|
|
||||||
note.updatedAt = new Date().toISOString()
|
|
||||||
|
|
||||||
this.index.update(id, note.title + '\n' + note.content)
|
|
||||||
|
|
||||||
await this.adapter.update(note)
|
|
||||||
|
|
||||||
return note
|
|
||||||
}
|
|
||||||
|
|
||||||
search(query) {
|
|
||||||
const ids = this.index.search(query)
|
|
||||||
return ids.map((id) => this.notesCache.get(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,11 @@ export default class PluginRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
list() {
|
list() {
|
||||||
return Array.from(this.plugins.values())
|
return Array.from(this.plugins.values()).map((plugin) => ({
|
||||||
|
id: plugin.id,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
configSchema: plugin.configSchema,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,20 @@ import { app, shell, BrowserWindow, ipcMain } from 'electron'
|
|||||||
import filesystemPlugin from '@takerofnotes/plugin-filesystem'
|
import filesystemPlugin from '@takerofnotes/plugin-filesystem'
|
||||||
import supabasePlugin from '@takerofnotes/plugin-supabase'
|
import supabasePlugin from '@takerofnotes/plugin-supabase'
|
||||||
import PluginRegistry from './core/PluginRegistry.js'
|
import PluginRegistry from './core/PluginRegistry.js'
|
||||||
import PluginConfig from './core/PluginConfig.js'
|
import Config from './core/Config.js'
|
||||||
import NotesAPI from './core/NotesAPI.js'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
|
const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 }
|
||||||
|
const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 }
|
||||||
|
|
||||||
const preloadPath = join(__dirname, '../preload/index.mjs')
|
const preloadPath = join(__dirname, '../preload/index.mjs')
|
||||||
const rendererPath = join(__dirname, '../renderer/index.html')
|
const rendererPath = join(__dirname, '../renderer/index.html')
|
||||||
|
|
||||||
// Main window
|
// Main window
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 354,
|
width: DEFAULT_WINDOW_SIZE.width,
|
||||||
height: 549,
|
height: DEFAULT_WINDOW_SIZE.height,
|
||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@@ -43,8 +45,8 @@ function createWindow() {
|
|||||||
// Open note in new window
|
// Open note in new window
|
||||||
function createNoteWindow(noteId) {
|
function createNoteWindow(noteId) {
|
||||||
const noteWindow = new BrowserWindow({
|
const noteWindow = new BrowserWindow({
|
||||||
width: 354,
|
width: DEFAULT_WINDOW_SIZE.width,
|
||||||
height: 549,
|
height: DEFAULT_WINDOW_SIZE.height,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: preloadPath,
|
preload: preloadPath,
|
||||||
@@ -56,11 +58,11 @@ function createNoteWindow(noteId) {
|
|||||||
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
noteWindow.loadURL(
|
noteWindow.loadURL(
|
||||||
`${process.env['ELECTRON_RENDERER_URL']}/note/${noteId}`,
|
`${process.env['ELECTRON_RENDERER_URL']}/#/note/${noteId}`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(rendererPath, {
|
noteWindow.loadFile(rendererPath, {
|
||||||
path: `/notes/${noteId}`,
|
hash: `/note/${noteId}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,13 @@ app.whenReady().then(async () => {
|
|||||||
createNoteWindow(noteId)
|
createNoteWindow(noteId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Broadcast note changes to all windows
|
||||||
|
const broadcastNoteChange = (event, data) => {
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => {
|
||||||
|
win.webContents.send(event, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Create plugin registry
|
// Create plugin registry
|
||||||
const registry = new PluginRegistry()
|
const registry = new PluginRegistry()
|
||||||
|
|
||||||
@@ -79,27 +88,81 @@ app.whenReady().then(async () => {
|
|||||||
registry.register(supabasePlugin)
|
registry.register(supabasePlugin)
|
||||||
|
|
||||||
// Pull plugin config
|
// Pull plugin config
|
||||||
const config = await new PluginConfig(filesystemPlugin).load()
|
const config = new Config(filesystemPlugin)
|
||||||
|
const initialConfig = await config.load()
|
||||||
|
|
||||||
// Create instance of active adapter
|
const setActivePlugin = async (pluginId) => {
|
||||||
// const plugin = registry.get(config.activeAdapter)
|
const currentConfig = await config.load()
|
||||||
const plugin = registry.get(supabasePlugin.id)
|
await config.write({ ...currentConfig, activeAdapter: pluginId })
|
||||||
// const adapter = plugin.createAdapter(config.adapterConfig)
|
|
||||||
const adapter = plugin.createAdapter({
|
const plugin = registry.get(pluginId)
|
||||||
supabaseKey: process.env.SUPABASE_KEY,
|
const adapterConfig = currentConfig.adapters[pluginId] || {}
|
||||||
supabaseUrl: process.env.SUPABASE_URL,
|
const adapter = plugin.createAdapter(adapterConfig)
|
||||||
|
|
||||||
|
// Initialize adapter
|
||||||
|
await adapter.init()
|
||||||
|
|
||||||
|
// Handle adapter methods via IPC
|
||||||
|
ipcMain.removeHandler('adapter:call')
|
||||||
|
ipcMain.handle('adapter:call', async (_, method, args) => {
|
||||||
|
if (!adapter[method]) {
|
||||||
|
throw new Error(`Invalid adapter method: ${method}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter[method](...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Init Notes API
|
broadcastNoteChange('plugin-changed', pluginId)
|
||||||
const notesAPI = new NotesAPI(adapter)
|
|
||||||
await notesAPI.init()
|
|
||||||
|
|
||||||
// Handle Notes API
|
return true
|
||||||
ipcMain.handle('notesAPI:call', (_, method, args) => {
|
}
|
||||||
if (!notesAPI[method]) {
|
|
||||||
throw new Error('Invalid method')
|
// Set active plugin
|
||||||
|
await setActivePlugin(initialConfig.activeAdapter)
|
||||||
|
|
||||||
|
// Get/set config
|
||||||
|
ipcMain.handle('getConfig', async () => {
|
||||||
|
return await config.load()
|
||||||
|
})
|
||||||
|
ipcMain.handle('setConfig', async (_, newConfig) => {
|
||||||
|
await config.write(newConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get/set plugins
|
||||||
|
ipcMain.handle('listPlugins', async () => {
|
||||||
|
return registry.list()
|
||||||
|
})
|
||||||
|
ipcMain.handle('setActivePlugin', async (_, pluginId) => {
|
||||||
|
return await setActivePlugin(pluginId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle note change events from renderer
|
||||||
|
ipcMain.on('note-changed', (_, event, data) => {
|
||||||
|
broadcastNoteChange(event, data)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle resizing for note "move" functionality
|
||||||
|
ipcMain.handle('move-opened', (_) => {
|
||||||
|
const activeWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
const windowSize = activeWindow.getSize()
|
||||||
|
|
||||||
|
if (windowSize[0] < DEFAULT_MOVE_WINDOW_SIZE.width) {
|
||||||
|
activeWindow.setSize(
|
||||||
|
DEFAULT_MOVE_WINDOW_SIZE.width,
|
||||||
|
DEFAULT_MOVE_WINDOW_SIZE.height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ipcMain.handle('move-closed', (_) => {
|
||||||
|
const activeWindow = BrowserWindow.getFocusedWindow()
|
||||||
|
const windowSize = activeWindow.getSize()
|
||||||
|
|
||||||
|
if (windowSize[0] === 708) {
|
||||||
|
activeWindow.setSize(
|
||||||
|
DEFAULT_WINDOW_SIZE.width,
|
||||||
|
DEFAULT_WINDOW_SIZE.height,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return notesAPI[method](...args)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
electronApp.setAppUserModelId('com.electron')
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|||||||
@@ -2,24 +2,50 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
|
getConfig: () => ipcRenderer.invoke('getConfig'),
|
||||||
|
setConfig: (config) => ipcRenderer.invoke('setConfig', config),
|
||||||
|
listPlugins: () => ipcRenderer.invoke('listPlugins'),
|
||||||
|
setActivePlugin: (pluginId) =>
|
||||||
|
ipcRenderer.invoke('setActivePlugin', pluginId),
|
||||||
openNoteWindow: (noteId) => {
|
openNoteWindow: (noteId) => {
|
||||||
ipcRenderer.send('open-note-window', noteId)
|
ipcRenderer.send('open-note-window', noteId)
|
||||||
},
|
},
|
||||||
|
onNoteCreated: (callback) => {
|
||||||
|
ipcRenderer.on('note-created', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
onNoteUpdated: (callback) => {
|
||||||
|
ipcRenderer.on('note-updated', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
onNoteDeleted: (callback) => {
|
||||||
|
ipcRenderer.on('note-deleted', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
onPluginChanged: (callback) => {
|
||||||
|
ipcRenderer.on('plugin-changed', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
notifyNoteChanged: (event, data) => {
|
||||||
|
ipcRenderer.send('note-changed', event, data)
|
||||||
|
},
|
||||||
|
moveOpened: () => {
|
||||||
|
ipcRenderer.invoke('move-opened')
|
||||||
|
},
|
||||||
|
moveClosed: () => {
|
||||||
|
ipcRenderer.invoke('move-closed')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement notes API
|
// Implement adapter API - communicates with plugin adapter in main process
|
||||||
const notesAPI = {
|
const adapter = {
|
||||||
call: (method, ...args) =>
|
call: (method, ...args) => ipcRenderer.invoke('adapter:call', method, args),
|
||||||
ipcRenderer.invoke('notesAPI:call', method, args),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
contextBridge.exposeInMainWorld('notesAPI', notesAPI)
|
contextBridge.exposeInMainWorld('adapter', adapter)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.api = api
|
window.api = api
|
||||||
|
window.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="classes" :style="styles">
|
<div :class="classes" :style="styles">
|
||||||
<Nav />
|
<div class="layout">
|
||||||
|
<div class="page">
|
||||||
|
<Nav v-if="$route.name !== 'note'" ref="nav" />
|
||||||
|
<Suspense>
|
||||||
<router-view :key="$route.fullPath" />
|
<router-view :key="$route.fullPath" />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MoveMenu />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Menu />
|
<Menu />
|
||||||
|
|
||||||
@@ -11,19 +18,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import loadFonts from '@fuzzco/font-loader'
|
import loadFonts from '@fuzzco/font-loader'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize, useElementBounding } from '@vueuse/core'
|
||||||
import Menu from '@/components/menu/Index.vue'
|
import Menu from '@/components/Menu.vue'
|
||||||
import Nav from '@/components/Nav.vue'
|
import Nav from '@/components/Nav.vue'
|
||||||
import ScrollBar from './components/ScrollBar.vue'
|
import MoveMenu from '@/components/MoveMenu.vue'
|
||||||
|
import ScrollBar from '@/components/ScrollBar.vue'
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
|
||||||
|
const nav = ref()
|
||||||
|
|
||||||
const { height } = useWindowSize()
|
const { height } = useWindowSize()
|
||||||
|
const { height: navHeight } = useElementBounding(nav)
|
||||||
|
const { config } = useConfig()
|
||||||
|
|
||||||
const classes = computed(() => [
|
const classes = computed(() => [
|
||||||
'container',
|
'container',
|
||||||
{ 'fonts-ready': !fontsLoading.value },
|
{ 'fonts-ready': !fontsLoading.value },
|
||||||
'theme-dark',
|
`theme-${config.value?.theme || 'dark'}`,
|
||||||
])
|
])
|
||||||
|
|
||||||
const fontsLoading = ref(true)
|
const fontsLoading = ref(true)
|
||||||
@@ -49,6 +62,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const styles = computed(() => ({
|
const styles = computed(() => ({
|
||||||
'--vh': height.value ? height.value / 100 + 'px' : '100vh',
|
'--vh': height.value ? height.value / 100 + 'px' : '100vh',
|
||||||
|
'--nav-height': navHeight.value + 'px',
|
||||||
}))
|
}))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -59,7 +73,17 @@ const styles = computed(() => ({
|
|||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
background: var(--theme-bg);
|
background: var(--theme-bg);
|
||||||
color: var(--theme-fg);
|
color: var(--theme-fg);
|
||||||
transition: opacity 1000ms;
|
transition: opacity 400ms;
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
min-height: calc(100 * var(--vh));
|
||||||
|
|
||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.fonts-ready) {
|
&:not(.fonts-ready) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,41 +1,132 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link class="category-row" :to="`/category/${category}`">
|
<component
|
||||||
|
:class="['category-row', { editable }]"
|
||||||
|
:to="`/category/${category}`"
|
||||||
|
:is="wrapper"
|
||||||
|
>
|
||||||
<span class="index">{{ String(index + 1).padStart(2, '0') }}.</span>
|
<span class="index">{{ String(index + 1).padStart(2, '0') }}.</span>
|
||||||
<span class="title h1">{{ category }}</span>
|
<form v-if="isEditing" @submit.prevent="onSave">
|
||||||
</router-link>
|
<input
|
||||||
|
v-model="categoryInput"
|
||||||
|
class="category-input"
|
||||||
|
type="text"
|
||||||
|
ref="input"
|
||||||
|
@blur="onSave"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<span v-else class="title h1">{{ categoryInput }}</span>
|
||||||
|
|
||||||
|
<button v-if="isEditing" class="save-button" @click="onSave">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button v-else-if="editable" class="edit-button" @click="onEdit">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({ index: Number, category: String })
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
index: Number,
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
default: () => '',
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: () => false,
|
||||||
|
},
|
||||||
|
wrapper: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['edited'])
|
||||||
|
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const categoryInput = ref('')
|
||||||
|
const input = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
categoryInput.value = props.category
|
||||||
|
|
||||||
|
if (categoryInput.value === '') {
|
||||||
|
onEdit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onEdit = async () => {
|
||||||
|
isEditing.value = true
|
||||||
|
await new Promise((res) => setTimeout(res, 300))
|
||||||
|
|
||||||
|
input.value?.focus()
|
||||||
|
}
|
||||||
|
const onSave = async () => {
|
||||||
|
isEditing.value = false
|
||||||
|
emit('edited', categoryInput.value)
|
||||||
|
await new Promise((res) => setTimeout(res, 300))
|
||||||
|
|
||||||
|
input.value?.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = computed(() => {
|
||||||
|
return props.wrapper || (props.editable ? 'div' : RouterLink)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.category-row {
|
.category-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: size-vw(26px) 1fr;
|
grid-template-columns: 26px 1fr auto;
|
||||||
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: size-vw(5px) 0 size-vw(15px);
|
padding: 8px 0 6px;
|
||||||
|
border-bottom: 1px dashed currentColor;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.index {
|
.index {
|
||||||
margin-top: size-vw(19px);
|
margin-top: 19px;
|
||||||
|
@include p;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@include line-clamp(2);
|
@include line-clamp(2);
|
||||||
}
|
}
|
||||||
&::after {
|
.category-input {
|
||||||
content: '----------------------------------------';
|
display: block;
|
||||||
position: absolute;
|
width: 100%;
|
||||||
bottom: 0;
|
@include h1;
|
||||||
left: 0;
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
&.router-link-exact-active {
|
}
|
||||||
|
.edit-button,
|
||||||
|
.save-button {
|
||||||
|
color: var(--grey-100);
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
.edit-button {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
&.editable:hover {
|
||||||
|
.edit-button {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.router-link-exact-active,
|
||||||
|
&.editable {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
&:hover:not(.router-link-exact-active) {
|
&:hover:not(.router-link-exact-active):not(.editable) {
|
||||||
color: var(--theme-accent);
|
color: var(--theme-accent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,20 @@
|
|||||||
<div v-if="menuOpen" class="menu" ref="container">
|
<div v-if="menuOpen" class="menu" ref="container">
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<div class="menu-wrap layout-block-inner">
|
<div class="menu-wrap layout-block">
|
||||||
<new-note class="menu-item" @noteOpened="closeMenu" />
|
<new-note class="menu-item" @noteOpened="closeMenu" />
|
||||||
<button class="menu-item">+ New Capitulum</button>
|
<router-link class="menu-item" to="/category">
|
||||||
<button class="menu-item">Change Theme</button>
|
+ New Capitulum
|
||||||
<router-link class="menu-item" to="/instructions"
|
</router-link>
|
||||||
>Instructio</router-link
|
<theme-switcher class="menu-item" />
|
||||||
>
|
<router-link class="menu-item" to="/instructions">
|
||||||
|
Instructio
|
||||||
|
</router-link>
|
||||||
<button class="menu-item">Import</button>
|
<button class="menu-item">Import</button>
|
||||||
<button class="menu-item">Export</button>
|
<button class="menu-item">Export</button>
|
||||||
|
<router-link class="menu-item" to="/preferences">
|
||||||
|
Preferences
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -19,9 +24,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import NewNote from '@/components/NewNote.vue'
|
import NewNote from '@/components/NewNote.vue'
|
||||||
|
import ThemeSwitcher from '@/components/ThemeSwitcher.vue'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import Nav from '../Nav.vue'
|
import Nav from './Nav.vue'
|
||||||
import useMenu from '@/composables/useMenu'
|
import useMenu from '@/composables/useMenu'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
@@ -58,12 +64,11 @@ const openNewCategory = () => {}
|
|||||||
.menu-wrap {
|
.menu-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: size-vw(3px);
|
padding-top: var(--nav-height);
|
||||||
padding-bottom: size-vw(10px);
|
padding-bottom: 10px;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: block;
|
padding: 16px 0;
|
||||||
padding: size-vw(16px) 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
64
src/renderer/src/components/MoveMenu.vue
Normal file
64
src/renderer/src/components/MoveMenu.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="open" class="move-menu layout-block">
|
||||||
|
<button class="cancel-button" @click="close">Cancel</button>
|
||||||
|
|
||||||
|
<template v-for="(category, i) in categories">
|
||||||
|
<category-row
|
||||||
|
v-if="category !== fromCategory"
|
||||||
|
:category="category"
|
||||||
|
:index="i"
|
||||||
|
wrapper="button"
|
||||||
|
@click="onCategoryClick(category)"
|
||||||
|
:key="category"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
|
import { computed, onBeforeUnmount, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import useState from '@/composables/useState'
|
||||||
|
import _omit from 'lodash/omit'
|
||||||
|
|
||||||
|
const { categories, updateNote } = useNotes()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const open = computed(() => route.query.move !== undefined)
|
||||||
|
const noteId = computed(() => route.query.move)
|
||||||
|
const fromCategory = computed(() => route.params.id)
|
||||||
|
|
||||||
|
const close = async () => {
|
||||||
|
await router.push({
|
||||||
|
query: _omit(route.query, ['move']),
|
||||||
|
})
|
||||||
|
|
||||||
|
await window.api.moveClosed()
|
||||||
|
}
|
||||||
|
const onCategoryClick = async (category) => {
|
||||||
|
if (!category || !noteId.value) return
|
||||||
|
|
||||||
|
await updateNote(noteId.value, { category: category })
|
||||||
|
|
||||||
|
await close()
|
||||||
|
}
|
||||||
|
watch(open, async () => {
|
||||||
|
if (!open.value) await close()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.move-menu {
|
||||||
|
width: 50vw;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 1px solid var(--grey-100);
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
color: var(--grey-100);
|
||||||
|
padding: 9px 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="nav layout-block-inner">
|
<nav class="nav layout-block">
|
||||||
|
<div class="left">
|
||||||
|
<router-link v-if="HAS_BACK_BUTTON.includes($route.name)" to="/"
|
||||||
|
><- Back</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
<button @click="toggleMenu">Menu</button>
|
<button @click="toggleMenu">Menu</button>
|
||||||
|
|
||||||
|
<router-link to="/search">Search</router-link>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useMenu from '@/composables/useMenu'
|
import useMenu from '@/composables/useMenu'
|
||||||
import { onMounted } from 'vue'
|
|
||||||
|
const HAS_BACK_BUTTON = [
|
||||||
|
'category',
|
||||||
|
'create-category',
|
||||||
|
'instructions',
|
||||||
|
'search',
|
||||||
|
'preferences',
|
||||||
|
]
|
||||||
|
|
||||||
const { menuOpen, closeMenu, openMenu } = useMenu()
|
const { menuOpen, closeMenu, openMenu } = useMenu()
|
||||||
|
|
||||||
@@ -17,19 +34,22 @@ const toggleMenu = () => {
|
|||||||
openMenu()
|
openMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Initialize menu state or perform any other necessary setup
|
|
||||||
// Example: Check if the user is logged in and update menu accordingly
|
|
||||||
// if (isLoggedIn()) {
|
|
||||||
// openMenu()
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.nav {
|
.nav {
|
||||||
padding-top: size-vw(9px);
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 9px;
|
||||||
|
padding-bottom: 16px;
|
||||||
color: var(--grey-100);
|
color: var(--grey-100);
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="note-row" @click="openNote(note.id)">
|
<div :class="['note-row', { 'move-active': moveActive }]">
|
||||||
<span class="date">{{ formatDate(note.createdAt) }}</span>
|
<span class="date">{{ formatDate(note.createdAt) }}</span>
|
||||||
<span class="title bold">{{ note.title }}</span>
|
<div class="title-actions">
|
||||||
|
<button class="title bold" @click="openNote(note.id)">
|
||||||
|
{{ note.title }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="action bold" @click="openNote(note.id)">
|
||||||
|
(open)
|
||||||
|
</button>
|
||||||
|
<button class="action bold move" @click="onMoveOpened">
|
||||||
|
(move)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useOpenNote from '@/composables/useOpenNote'
|
import useOpenNote from '@/composables/useOpenNote'
|
||||||
|
import useState from '@/composables/useState'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { computed } from 'vue'
|
||||||
import { format } from 'fecha'
|
import { format } from 'fecha'
|
||||||
|
|
||||||
const props = defineProps({ note: Object })
|
const props = defineProps({ note: Object })
|
||||||
@@ -17,35 +31,56 @@ const formatDate = (date) => {
|
|||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
return format(d, 'MM/DD/YYYY')
|
return format(d, 'MM/DD/YYYY')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moving
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const onMoveOpened = async () => {
|
||||||
|
await window.api.moveOpened()
|
||||||
|
await router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
move: props.note.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.log(route.query)
|
||||||
|
}
|
||||||
|
const moveActive = computed(() => route.query.move === props.note.id)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.note-row {
|
.note-row {
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: size-vw(20px);
|
width: 100%;
|
||||||
cursor: pointer;
|
gap: 20px;
|
||||||
|
|
||||||
.title {
|
.title-actions {
|
||||||
width: size-vw(159px);
|
display: grid;
|
||||||
position: relative;
|
grid-template-columns: 1fr auto auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
&::after {
|
.action {
|
||||||
content: '(open)';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
font-weight: 700;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
&:not(:hover) {
|
||||||
|
color: var(--grey-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
}
|
||||||
|
&:hover,
|
||||||
|
&.move-active {
|
||||||
color: var(--theme-accent);
|
color: var(--theme-accent);
|
||||||
|
|
||||||
.title::after {
|
.title-actions .action {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.move-active {
|
||||||
|
.title-actions .move {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
24
src/renderer/src/components/PageLoading.vue
Normal file
24
src/renderer/src/components/PageLoading.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-loading">
|
||||||
|
<svg-spinner />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgSpinner from '@/components/svg/Spinner.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.page-loading {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -75,7 +75,7 @@ watch(
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: size-vw(8px);
|
width: 8px;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
border-left: 1px solid var(--grey-100);
|
border-left: 1px solid var(--grey-100);
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ watch(
|
|||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: size-vw(388px);
|
height: 388px;
|
||||||
background: var(--grey-100);
|
background: var(--grey-100);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
74
src/renderer/src/components/SearchInput.vue
Normal file
74
src/renderer/src/components/SearchInput.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-input">
|
||||||
|
<input
|
||||||
|
v-model="model"
|
||||||
|
type="text"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
ref="input"
|
||||||
|
@input="emit('input', model.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
placeholder: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const model = defineModel()
|
||||||
|
|
||||||
|
const emit = defineEmits(['input'])
|
||||||
|
|
||||||
|
const input = ref()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus: () => input.value.focus(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.search-input {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 5px 15px 6px;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
--clip-start: 16px;
|
||||||
|
clip-path: polygon(
|
||||||
|
var(--clip-start) 1px,
|
||||||
|
calc(100% - var(--clip-start)) 1px,
|
||||||
|
calc(100% - 1.5px) 50%,
|
||||||
|
calc(100% - var(--clip-start)) calc(100% - 1px),
|
||||||
|
var(--clip-start) calc(100% - 1px),
|
||||||
|
1.5px 50%
|
||||||
|
);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--grey-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--theme-fg);
|
||||||
|
--clip-start: 15px;
|
||||||
|
clip-path: polygon(
|
||||||
|
var(--clip-start) 0,
|
||||||
|
calc(100% - var(--clip-start)) 0,
|
||||||
|
100% 50%,
|
||||||
|
calc(100% - var(--clip-start)) 100%,
|
||||||
|
var(--clip-start) 100%,
|
||||||
|
0% 50%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/renderer/src/components/ThemeSwitcher.vue
Normal file
49
src/renderer/src/components/ThemeSwitcher.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="config" class="theme-switcher">
|
||||||
|
<span>Change Theme</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-for="(value, key) in themes"
|
||||||
|
:class="[`theme-${key}`, { active: config.theme === key }]"
|
||||||
|
@click="setTheme(key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
import { themes } from '@/libs/theme'
|
||||||
|
|
||||||
|
const { config } = useConfig()
|
||||||
|
|
||||||
|
const setTheme = (value) => {
|
||||||
|
if (!config.value) return
|
||||||
|
config.value.theme = value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.theme-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--theme-bg);
|
||||||
|
display: block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 1px solid var(--theme-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-fg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/renderer/src/components/note/Download.vue
Normal file
61
src/renderer/src/components/note/Download.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="note-download">
|
||||||
|
<button @click="download">{{ noteTitle }}.md ↓</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import _kebabCase from 'lodash/kebabCase'
|
||||||
|
|
||||||
|
const DEFAULT_TITLE = 'Untitled'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const noteTitle = computed(() => {
|
||||||
|
if (!props.editor) return DEFAULT_TITLE
|
||||||
|
|
||||||
|
let title
|
||||||
|
const doc = props.editor.state.doc
|
||||||
|
|
||||||
|
const firstNode = doc.firstChild
|
||||||
|
if (!firstNode || firstNode.type.name !== 'heading') title = DEFAULT_TITLE
|
||||||
|
|
||||||
|
title = firstNode.textContent.trim() || DEFAULT_TITLE
|
||||||
|
|
||||||
|
return _kebabCase(title)
|
||||||
|
})
|
||||||
|
|
||||||
|
const download = () => {
|
||||||
|
if (!props.editor) return
|
||||||
|
|
||||||
|
const content = props.editor.getMarkdown()
|
||||||
|
const blob = new Blob([content], { type: 'text/plain' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${noteTitle.value}.md`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-download {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: var(--grey-100);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
259
src/renderer/src/components/note/Editor.vue
Normal file
259
src/renderer/src/components/note/Editor.vue
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="editor" class="note-editor">
|
||||||
|
<editor-content :editor="editor" class="editor-wrap" />
|
||||||
|
|
||||||
|
<editor-menu :editor="editor" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<page-loading v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||||
|
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
||||||
|
import { TaskList, TaskItem } from '@tiptap/extension-list'
|
||||||
|
import { Highlight } from '@tiptap/extension-highlight'
|
||||||
|
import PageLoading from '@/components/PageLoading.vue'
|
||||||
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import { Placeholder } from '@tiptap/extensions'
|
||||||
|
import { all, createLowlight } from 'lowlight'
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import _debounce from 'lodash/debounce'
|
||||||
|
import { Markdown } from '@tiptap/markdown'
|
||||||
|
import EditorMenu from './Menu.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const editor = shallowRef()
|
||||||
|
|
||||||
|
const { loadNote, updateNote } = useNotes()
|
||||||
|
|
||||||
|
const onUpdate = _debounce(async ({ editor }) => {
|
||||||
|
const json = editor.getJSON()
|
||||||
|
const text = editor.getText()
|
||||||
|
|
||||||
|
// Get doc title
|
||||||
|
let title
|
||||||
|
const doc = editor.state.doc
|
||||||
|
|
||||||
|
const firstNode = doc.firstChild
|
||||||
|
if (!firstNode || firstNode.type.name !== 'heading') title = 'Untitled'
|
||||||
|
|
||||||
|
title = firstNode.textContent.trim() || 'Untitled'
|
||||||
|
|
||||||
|
await updateNote(props.id, {
|
||||||
|
title,
|
||||||
|
content: json,
|
||||||
|
plainText: text,
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const note = await loadNote(props.id)
|
||||||
|
|
||||||
|
// Lowlight setup
|
||||||
|
const lowlight = createLowlight(all)
|
||||||
|
|
||||||
|
// Force note format
|
||||||
|
const CustomDocument = Document.extend({
|
||||||
|
content: 'heading block*',
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.value = new Editor({
|
||||||
|
extensions: [
|
||||||
|
CustomDocument,
|
||||||
|
StarterKit.configure({
|
||||||
|
document: false,
|
||||||
|
heading: { levels: [1] },
|
||||||
|
trailingNode: {
|
||||||
|
node: 'paragraph',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: ({ node }) => {
|
||||||
|
if (node.type.name === 'heading') {
|
||||||
|
return 'Title'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TaskList,
|
||||||
|
TaskItem,
|
||||||
|
Highlight,
|
||||||
|
Markdown,
|
||||||
|
CodeBlockLowlight.configure({
|
||||||
|
lowlight,
|
||||||
|
enableTabIndentation: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content: note.content || [],
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editor.value?.destroy?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
editor,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-editor {
|
||||||
|
h1 {
|
||||||
|
font-weight: 700 !important;
|
||||||
|
@include p;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
@include drop-cap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1.is-editor-empty:first-child::before {
|
||||||
|
color: var(--grey-100);
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
pointer-events: none;
|
||||||
|
@include drop-cap;
|
||||||
|
}
|
||||||
|
p strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
p em {
|
||||||
|
/* font-style: italic; */
|
||||||
|
color: var(--grey-100);
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 1px dashed currentColor;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: list-item;
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
*:not(:last-child) {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: list-item;
|
||||||
|
margin-left: 1.75em;
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
@include p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li:not(:last-child) {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--theme-link);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
color: var(--theme-accent);
|
||||||
|
padding: 0 0.2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
display: block;
|
||||||
|
color: inherit;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid var(--grey-100);
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
s {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mark {
|
||||||
|
background: var(--theme-accent);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type='taskList'] {
|
||||||
|
list-style: none;
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '✓';
|
||||||
|
font-size: 1.5em;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
&:checked::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul[data-type='taskList'] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
242
src/renderer/src/components/note/Find.vue
Normal file
242
src/renderer/src/components/note/Find.vue
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="note-find">
|
||||||
|
<div class="find-bar">
|
||||||
|
<!-- <input
|
||||||
|
@keydown.enter="findNext"
|
||||||
|
@keydown.escape="close"
|
||||||
|
ref="inputRef"
|
||||||
|
/> -->
|
||||||
|
<search-input
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="Find..."
|
||||||
|
ref="inputRef"
|
||||||
|
/>
|
||||||
|
<span class="match-count">
|
||||||
|
{{ currentMatch + 1 }} / {{ matches.length }}
|
||||||
|
</span>
|
||||||
|
<button @click="findPrev" :disabled="matches.length === 0">
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button @click="findNext" :disabled="matches.length === 0">
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
<button @click="close">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, nextTick } from 'vue'
|
||||||
|
import SearchInput from '@/components/SearchInput.vue'
|
||||||
|
import { Highlight } from '@tiptap/extension-highlight'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const matches = ref([])
|
||||||
|
const currentMatch = ref(0)
|
||||||
|
const inputRef = ref(null)
|
||||||
|
|
||||||
|
const findInDocument = () => {
|
||||||
|
if (!props.editor || !searchQuery.value) {
|
||||||
|
clearHighlights()
|
||||||
|
matches.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHighlights()
|
||||||
|
|
||||||
|
const query = searchQuery.value.toLowerCase()
|
||||||
|
const doc = props.editor.state.doc
|
||||||
|
const foundMatches = []
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText || !node.text) return
|
||||||
|
|
||||||
|
const text = node.text.toLowerCase()
|
||||||
|
let start = 0
|
||||||
|
let index
|
||||||
|
|
||||||
|
while ((index = text.indexOf(query, start)) !== -1) {
|
||||||
|
foundMatches.push({
|
||||||
|
from: pos + index,
|
||||||
|
to: pos + index + query.length,
|
||||||
|
})
|
||||||
|
start = index + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
matches.value = foundMatches
|
||||||
|
currentMatch.value = 0
|
||||||
|
|
||||||
|
if (foundMatches.length > 0) {
|
||||||
|
highlightMatch(0)
|
||||||
|
scrollToMatch(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightMatch = (index) => {
|
||||||
|
if (!props.editor || !matches.value[index]) return
|
||||||
|
|
||||||
|
const { from, to } = matches.value[index]
|
||||||
|
|
||||||
|
props.editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection({ from, to })
|
||||||
|
.setHighlight({ color: 'var(--theme-accent)' })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
inputRef.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearHighlights = () => {
|
||||||
|
if (!props.editor) return
|
||||||
|
|
||||||
|
const { state } = props.editor
|
||||||
|
const { doc } = state
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (node.marks && node.marks.length > 0) {
|
||||||
|
node.marks.forEach((mark) => {
|
||||||
|
if (mark.type.name === 'highlight') {
|
||||||
|
props.editor.chain().focus().unsetHighlight().run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
props.editor.commands.unsetHighlight()
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToMatch = (index) => {
|
||||||
|
if (!props.editor || !matches.value[index]) return
|
||||||
|
|
||||||
|
const { from } = matches.value[index]
|
||||||
|
props.editor.commands.setTextSelection({ from, to: from })
|
||||||
|
|
||||||
|
const dom = props.editor.view.dom
|
||||||
|
const selection = window.getSelection()
|
||||||
|
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
range.commonAncestorContainer.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findNext = () => {
|
||||||
|
if (matches.value.length === 0) return
|
||||||
|
|
||||||
|
clearHighlights()
|
||||||
|
currentMatch.value = (currentMatch.value + 1) % matches.value.length
|
||||||
|
highlightMatch(currentMatch.value)
|
||||||
|
scrollToMatch(currentMatch.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const findPrev = () => {
|
||||||
|
if (matches.value.length === 0) return
|
||||||
|
|
||||||
|
clearHighlights()
|
||||||
|
currentMatch.value =
|
||||||
|
(currentMatch.value - 1 + matches.value.length) % matches.value.length
|
||||||
|
highlightMatch(currentMatch.value)
|
||||||
|
scrollToMatch(currentMatch.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
clearHighlights()
|
||||||
|
searchQuery.value = ''
|
||||||
|
matches.value = []
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
await nextTick()
|
||||||
|
inputRef.value?.focus()
|
||||||
|
if (props.editor && searchQuery.value) {
|
||||||
|
console.log('visible change')
|
||||||
|
findInDocument()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.editor,
|
||||||
|
(newEditor) => {
|
||||||
|
if (newEditor && props.visible && searchQuery.value) {
|
||||||
|
console.log('editor change')
|
||||||
|
findInDocument()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(searchQuery, () => {
|
||||||
|
console.log('search change')
|
||||||
|
findInDocument()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-find {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.find-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
border-bottom: 1px solid var(--grey-100);
|
||||||
|
padding: 12px 10px;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.match-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--grey-100);
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--theme-fg);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--grey-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/renderer/src/components/note/Menu.vue
Normal file
61
src/renderer/src/components/note/Menu.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<bubble-menu v-if="editor" :editor="editor">
|
||||||
|
<div class="note-menu">
|
||||||
|
<button
|
||||||
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ active: editor.isActive('bold') }"
|
||||||
|
>
|
||||||
|
Bold
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
|
:class="{ active: editor.isActive('italic') }"
|
||||||
|
>
|
||||||
|
Italic
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="editor.chain().focus().toggleHighlight().run()"
|
||||||
|
:class="{ active: editor.isActive('highlight') }"
|
||||||
|
>
|
||||||
|
Highlight
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bubble-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { BubbleMenu } from '@tiptap/vue-3/menus'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-menu {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
color: var(--grey-100);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--grey-100);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: var(--theme-fg);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
src/renderer/src/components/svg/Spinner.vue
Normal file
32
src/renderer/src/components/svg/Spinner.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="svg-spinner"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
style="margin: auto; display: block"
|
||||||
|
width="18px"
|
||||||
|
height="18px"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="xMidYMid"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="40"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-dasharray="62 62"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
dur="1s"
|
||||||
|
keyTimes="0;1"
|
||||||
|
values="0 50 50;360 50 50"
|
||||||
|
></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg
|
|
||||||
class="svg-icon-hr"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M2 11H4V13H2V11ZM6 11H18V13H6V11ZM20 11H22V13H20V11Z"></path>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
42
src/renderer/src/composables/useConfig.js
Normal file
42
src/renderer/src/composables/useConfig.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { ref, watch, toRaw, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const config = ref()
|
||||||
|
let configResolve = null
|
||||||
|
const configPromise = new Promise((resolve) => {
|
||||||
|
configResolve = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
onMounted(async () => {
|
||||||
|
if (config.value) {
|
||||||
|
configResolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.value = await window.api.getConfig()
|
||||||
|
configResolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
config,
|
||||||
|
async (newValue) => {
|
||||||
|
await window.api.setConfig(toRaw(newValue))
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const ensureConfig = async () => {
|
||||||
|
if (config.value) return config.value
|
||||||
|
return configPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshConfig = async () => {
|
||||||
|
config.value = await window.api.getConfig()
|
||||||
|
configResolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
ensureConfig,
|
||||||
|
refreshConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/renderer/src/composables/useEnvironment.ts
Normal file
41
src/renderer/src/composables/useEnvironment.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export enum ENVIRONMENTS {
|
||||||
|
ELECTRON = 'electron',
|
||||||
|
WEB = 'web',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEnvironment = (): ENVIRONMENTS => {
|
||||||
|
function isElectron() {
|
||||||
|
// Renderer process
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
typeof window.process === 'object' &&
|
||||||
|
window.process.type === 'renderer'
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main process
|
||||||
|
if (
|
||||||
|
typeof process !== 'undefined' &&
|
||||||
|
typeof process.versions === 'object' &&
|
||||||
|
!!process.versions.electron
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
|
if (
|
||||||
|
typeof navigator === 'object' &&
|
||||||
|
typeof navigator.userAgent === 'string' &&
|
||||||
|
navigator.userAgent.indexOf('Electron') >= 0
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const environment = isElectron() ? ENVIRONMENTS.ELECTRON : ENVIRONMENTS.WEB
|
||||||
|
|
||||||
|
return environment
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { inject, onBeforeUnmount } from 'vue'
|
|
||||||
|
|
||||||
export default (callback = () => {}, instanceId) => {
|
|
||||||
const instanceKey = `lenis${instanceId ? `-${instanceId}` : ''}`
|
|
||||||
const lenis = inject(instanceKey)
|
|
||||||
|
|
||||||
if (lenis.value) {
|
|
||||||
lenis.value.on('scroll', callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => lenis.value?.off('scroll', callback))
|
|
||||||
|
|
||||||
return lenis
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ export default () => {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const menuOpen = computed(() => route.query.menuOpen === 'true')
|
const menuOpen = computed(() => route.query?.menuOpen === 'true')
|
||||||
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@@ -1,74 +1,142 @@
|
|||||||
|
import _omit from 'lodash/omit'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { getNotesAPI } from '@/libs/core/getNotesAPI'
|
||||||
|
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const searchResults = ref([])
|
const searchResults = ref([])
|
||||||
|
const notesChangeCount = ref(0)
|
||||||
|
|
||||||
|
let listenersInitialized = false
|
||||||
|
|
||||||
|
const setupListeners = () => {
|
||||||
|
if (listenersInitialized || typeof window === 'undefined') return
|
||||||
|
listenersInitialized = true
|
||||||
|
|
||||||
|
const updateCacheCount = async (note) => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
await api.updateNote(
|
||||||
|
note.id,
|
||||||
|
_omit(note, ['id', 'createdAt', 'updatedAt']),
|
||||||
|
)
|
||||||
|
|
||||||
|
notesChangeCount.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
window.api.onNoteCreated(updateCacheCount)
|
||||||
|
window.api.onNoteUpdated(updateCacheCount)
|
||||||
|
window.api.onPluginChanged(async () => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
await api.init()
|
||||||
|
|
||||||
|
notesChangeCount.value++
|
||||||
|
})
|
||||||
|
|
||||||
|
// Todo update cache
|
||||||
|
window.api.onNoteDeleted(() => {
|
||||||
|
notesChangeCount.value++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastChange = (event, data) => {
|
||||||
|
window.api.notifyNoteChanged(event, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners()
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Initialization
|
Initialization
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function loadCategories() {
|
const loadCategories = async () => {
|
||||||
categories.value = await window.notesAPI.call('getCategories')
|
const api = await getNotesAPI()
|
||||||
|
categories.value = api.getCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCategoryNotes(category = null) {
|
const loadCategoryNotes = async (category = null) => {
|
||||||
return await window.notesAPI.call('getCategoryNotes', category)
|
const api = await getNotesAPI()
|
||||||
|
return api.getCategoryNotes(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNote(id) {
|
const loadNote = async (id) => {
|
||||||
return await window.notesAPI.call('getNote', id)
|
const api = await getNotesAPI()
|
||||||
|
return api.getNote(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Create
|
Create
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function createNote(metadata, content) {
|
const createNote = async (metadata, content, plainText = '') => {
|
||||||
const note = await window.notesAPI.call('createNote', metadata, content)
|
const api = await getNotesAPI()
|
||||||
|
const note = await api.createNote(metadata, content, plainText)
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
|
broadcastChange('note-created', note)
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Update
|
Update
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function updateNoteContent(id, content) {
|
const updateNote = async (id, updates) => {
|
||||||
const note = await window.notesAPI.call('updateNote', id, content)
|
const api = await getNotesAPI()
|
||||||
|
|
||||||
|
const note = await api.updateNote(id, updates)
|
||||||
|
|
||||||
|
if (updates.category !== undefined || updates.title !== undefined) {
|
||||||
|
await loadCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastChange('note-updated', note)
|
||||||
|
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNoteMetadata(id, updates) {
|
const updateCategory = async (category, update) => {
|
||||||
const note = await window.notesAPI.call(
|
const notes = await loadCategoryNotes(category)
|
||||||
'updateNoteMetadata',
|
|
||||||
id,
|
for (const note of notes) {
|
||||||
updates,
|
await updateNote(note.id, { category: update })
|
||||||
)
|
}
|
||||||
|
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
return note
|
}
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
Delete
|
||||||
|
--------------------------*/
|
||||||
|
const deleteNote = async (id) => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
await api.deleteNote(id)
|
||||||
|
await loadCategories()
|
||||||
|
broadcastChange('note-deleted', { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Search
|
Search
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function search(query) {
|
const search = async (query) => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
searchResults.value = []
|
searchResults.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResults.value = await window.notesAPI.call('search', query)
|
searchResults.value = api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
searchResults,
|
searchResults,
|
||||||
|
notesChangeCount,
|
||||||
|
|
||||||
loadCategories,
|
loadCategories,
|
||||||
loadCategoryNotes,
|
loadCategoryNotes,
|
||||||
loadNote,
|
loadNote,
|
||||||
|
|
||||||
createNote,
|
createNote,
|
||||||
updateNoteContent,
|
updateNote,
|
||||||
updateNoteMetadata,
|
updateCategory,
|
||||||
|
deleteNote,
|
||||||
|
|
||||||
search,
|
search,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useEnvironment, ENVIRONMENTS } from './useEnvironment'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -6,13 +7,9 @@ export default () => {
|
|||||||
function openNote(noteId, options = {}) {
|
function openNote(noteId, options = {}) {
|
||||||
const { newWindow = true } = options
|
const { newWindow = true } = options
|
||||||
|
|
||||||
// Electron environment check
|
const environment = useEnvironment()
|
||||||
const isElectron =
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
window.api &&
|
|
||||||
typeof window.api.openNoteWindow === 'function'
|
|
||||||
|
|
||||||
if (newWindow && isElectron) {
|
if (newWindow && environment === ENVIRONMENTS.ELECTRON) {
|
||||||
window.api.openNoteWindow(noteId)
|
window.api.openNoteWindow(noteId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/renderer/src/composables/usePlugins.js
Normal file
20
src/renderer/src/composables/usePlugins.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import useConfig from './useConfig'
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const { refreshConfig } = useConfig()
|
||||||
|
|
||||||
|
const plugins = ref([])
|
||||||
|
|
||||||
|
plugins.value = await window.api.listPlugins()
|
||||||
|
|
||||||
|
const setActivePlugin = async (pluginId) => {
|
||||||
|
await window.api.setActivePlugin(pluginId)
|
||||||
|
await refreshConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
setActivePlugin,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useWindowSize } from '@vueuse/core'
|
|
||||||
import { viewports } from '@/libs/theme'
|
|
||||||
|
|
||||||
const { width: wWidth, height: wHeight } = useWindowSize()
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
// Desktop
|
|
||||||
const dvw = (pixels) => {
|
|
||||||
return (pixels / viewports.desktop.width) * wWidth.value
|
|
||||||
}
|
|
||||||
const dvh = (pixels) => {
|
|
||||||
return (pixels / viewports.desktop.height) * wHeight.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile
|
|
||||||
const mvw = (pixels) => {
|
|
||||||
return (pixels / viewports.mobile.width) * wWidth.value
|
|
||||||
}
|
|
||||||
const mvh = (pixels) => {
|
|
||||||
return (pixels / viewports.mobile.height) * wHeight.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
dvw,
|
|
||||||
dvh,
|
|
||||||
mvw,
|
|
||||||
mvh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import {
|
|
||||||
useElementBounding,
|
|
||||||
useIntersectionObserver,
|
|
||||||
useWindowSize,
|
|
||||||
} from '@vueuse/core'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { mapRange, clamp } from '@/libs/math'
|
|
||||||
import useLenis from '@/composables/useLenis'
|
|
||||||
|
|
||||||
const { height: wHeight } = useWindowSize()
|
|
||||||
|
|
||||||
export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
|
|
||||||
const isActive = ref(true)
|
|
||||||
const smoothProgress = ref(0)
|
|
||||||
|
|
||||||
const { height, top } = useElementBounding(el)
|
|
||||||
|
|
||||||
const isIntersected = ref(false)
|
|
||||||
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
|
|
||||||
isIntersected.value = isIntersecting
|
|
||||||
})
|
|
||||||
|
|
||||||
useLenis(({ scroll }) => {
|
|
||||||
if (!isActive.value) return
|
|
||||||
if (!height.value || !wHeight.value) return
|
|
||||||
if (!isIntersected.value) return
|
|
||||||
|
|
||||||
const pageTop = scroll + top.value
|
|
||||||
|
|
||||||
const start = pageTop - wHeight.value * entry
|
|
||||||
const end = pageTop + height.value - wHeight.value * exit
|
|
||||||
|
|
||||||
let rawProgress = mapRange(start, end, scroll, 0, 1)
|
|
||||||
rawProgress = clamp(0, rawProgress, 1)
|
|
||||||
|
|
||||||
smoothProgress.value += (rawProgress - smoothProgress.value) * 0.1
|
|
||||||
callback?.(smoothProgress.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
isActive.value = false
|
|
||||||
stop?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
return { destroy }
|
|
||||||
}
|
|
||||||
8
src/renderer/src/composables/useState.js
Normal file
8
src/renderer/src/composables/useState.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createGlobalState } from '@vueuse/core'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export default createGlobalState(() => {
|
||||||
|
const moveMenuOpen = ref(false)
|
||||||
|
|
||||||
|
return { moveMenuOpen }
|
||||||
|
})
|
||||||
28
src/renderer/src/content/instructions.md
Normal file
28
src/renderer/src/content/instructions.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Medieval Translation
|
||||||
|
|
||||||
|
Nota = Note\
|
||||||
|
Capitulum = Category\
|
||||||
|
Intructio = Instructions\
|
||||||
|
Tabula = Index/Overview
|
||||||
|
|
||||||
|
\*This can be disabled via toolbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Program Key Commands
|
||||||
|
|
||||||
|
cmd + s = save\
|
||||||
|
cmd + t = new capitulum\
|
||||||
|
cmd + n = new nota\
|
||||||
|
cmd + x = close window\
|
||||||
|
dbl click = change name / open nota\
|
||||||
|
paste hyperlink twice = activated url
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Text Markdowns
|
||||||
|
|
||||||
|
cmd + b = Bold\
|
||||||
|
cmd + u = underline\
|
||||||
|
--- = ---------- (ruled line break)\
|
||||||
|
/_text_/ = Desaturated text
|
||||||
25
src/renderer/src/libs/core/IpcAdapter.js
Normal file
25
src/renderer/src/libs/core/IpcAdapter.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default class IpcAdapter {
|
||||||
|
constructor() {
|
||||||
|
this._methods = ['init', 'getAll', 'create', 'update', 'delete']
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
return await window.adapter.call('init')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
return await window.adapter.call('getAll')
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(note) {
|
||||||
|
return await window.adapter.call('create', note)
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(note) {
|
||||||
|
return await window.adapter.call('update', note)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
return await window.adapter.call('delete', id)
|
||||||
|
}
|
||||||
|
}
|
||||||
247
src/renderer/src/libs/core/NotesAPI.js
Normal file
247
src/renderer/src/libs/core/NotesAPI.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import sodium from 'libsodium-wrappers'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { Index } from 'flexsearch'
|
||||||
|
import * as uint from '@/libs/uint'
|
||||||
|
|
||||||
|
export default class NotesAPI {
|
||||||
|
constructor(adapter, encryptionKey = null) {
|
||||||
|
if (!adapter) {
|
||||||
|
throw new Error('NotesAPI requires a storage adapter')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adapter = adapter
|
||||||
|
this.notesCache = new Map()
|
||||||
|
this.encryptionKey = encryptionKey
|
||||||
|
this._sodiumReady = false
|
||||||
|
|
||||||
|
this.index = new Index({
|
||||||
|
tokenize: 'forward',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initSodium() {
|
||||||
|
if (!this._sodiumReady) {
|
||||||
|
await sodium.ready
|
||||||
|
this._sodiumReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_encrypt(note) {
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
throw new Error('Encryption key not set')
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = uint.hexToUint8Array(this.encryptionKey)
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error(
|
||||||
|
'Encryption key must be 64 hex characters (32 bytes)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
const message = JSON.stringify(note)
|
||||||
|
|
||||||
|
const ciphertext = sodium.crypto_secretbox_easy(
|
||||||
|
new TextEncoder().encode(message),
|
||||||
|
nonce,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
|
||||||
|
const combined = uint.concatUint8Arrays(nonce, ciphertext)
|
||||||
|
return uint.uint8ArrayToBase64(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
_decrypt(encryptedData) {
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
throw new Error('Encryption key not set')
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = uint.hexToUint8Array(this.encryptionKey)
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error(
|
||||||
|
'Encryption key must be 64 hex characters (32 bytes)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let combined
|
||||||
|
try {
|
||||||
|
combined = uint.base64ToUint8Array(encryptedData)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Invalid encrypted data: not valid base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
combined.length <
|
||||||
|
sodium.crypto_secretbox_NONCEBYTES +
|
||||||
|
sodium.crypto_secretbox_MACBYTES
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid encrypted data: too short')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
|
||||||
|
let decrypted
|
||||||
|
try {
|
||||||
|
decrypted = sodium.crypto_secretbox_open_easy(
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Decryption failed: wrong key or corrupted data')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decrypted) {
|
||||||
|
throw new Error('Decryption failed: no data returned')
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedStr = new TextDecoder().decode(decrypted)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedStr)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`Decryption succeeded but invalid JSON: ${decryptedStr}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this._initSodium()
|
||||||
|
await this.adapter.init()
|
||||||
|
this.notesCache.clear()
|
||||||
|
|
||||||
|
const encryptedNotes = await this.adapter.getAll()
|
||||||
|
|
||||||
|
for (const encryptedNote of encryptedNotes) {
|
||||||
|
try {
|
||||||
|
const note = this._decrypt(encryptedNote.data || encryptedNote)
|
||||||
|
|
||||||
|
this.notesCache.set(note.id, note)
|
||||||
|
const searchText =
|
||||||
|
note.plainText || this._extractPlainText(note.content)
|
||||||
|
this.index.add(note.id, note.title + '\n' + searchText)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt note:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_extractPlainText(content) {
|
||||||
|
if (!content) return ''
|
||||||
|
if (typeof content === 'string') return content
|
||||||
|
|
||||||
|
const extractText = (node) => {
|
||||||
|
if (typeof node === 'string') return node
|
||||||
|
if (!node || !node.content) return ''
|
||||||
|
return node.content.map(extractText).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractText(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------
|
||||||
|
Public API
|
||||||
|
------------------------*/
|
||||||
|
getCategories() {
|
||||||
|
const categories = new Set()
|
||||||
|
|
||||||
|
for (const note of this.notesCache.values()) {
|
||||||
|
if (note.category) {
|
||||||
|
categories.add(note.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(categories).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryNotes(categoryName = null) {
|
||||||
|
return Array.from(this.notesCache.values())
|
||||||
|
.filter((n) => n.category === categoryName)
|
||||||
|
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||||
|
.map((n) => ({ ...n }))
|
||||||
|
}
|
||||||
|
|
||||||
|
getNote(id) {
|
||||||
|
const note = this.notesCache.get(id)
|
||||||
|
return note ? { ...note } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNote(metadata = {}, content = '', plainText = '') {
|
||||||
|
const id = uuidv4()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
const note = {
|
||||||
|
id,
|
||||||
|
title: metadata.title || 'Untitled',
|
||||||
|
category: metadata.category || null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
content,
|
||||||
|
plainText,
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedNote = {
|
||||||
|
id: note.id,
|
||||||
|
data: this._encrypt(note),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notesCache.set(id, note)
|
||||||
|
this.index.add(id, note.title + '\n' + plainText)
|
||||||
|
|
||||||
|
await this.adapter.create(encryptedNote)
|
||||||
|
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNote(id) {
|
||||||
|
await this.adapter.delete(id)
|
||||||
|
|
||||||
|
this.notesCache.delete(id)
|
||||||
|
this.index.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNote(id, updates = {}) {
|
||||||
|
const note = this.notesCache.get(id)
|
||||||
|
if (!note) throw new Error('Note not found')
|
||||||
|
|
||||||
|
const allowedFields = ['title', 'category', 'content', 'plainText']
|
||||||
|
|
||||||
|
for (const key of Object.keys(updates)) {
|
||||||
|
if (!allowedFields.includes(key)) {
|
||||||
|
throw new Error(`Invalid update field: ${key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedNote = {
|
||||||
|
...note,
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedNote = {
|
||||||
|
id: updatedNote.id,
|
||||||
|
data: this._encrypt(updatedNote),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notesCache.set(id, updatedNote)
|
||||||
|
|
||||||
|
const searchText =
|
||||||
|
updatedNote.plainText || this._extractPlainText(updatedNote.content)
|
||||||
|
|
||||||
|
this.index.update(id, updatedNote.title + '\n' + searchText)
|
||||||
|
|
||||||
|
await this.adapter.update(encryptedNote)
|
||||||
|
|
||||||
|
return updatedNote
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query) {
|
||||||
|
const ids = this.index.search(query, {
|
||||||
|
limit: 50,
|
||||||
|
suggest: true,
|
||||||
|
})
|
||||||
|
return ids.map((id) => this.notesCache.get(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/renderer/src/libs/core/getNotesAPI.js
Normal file
47
src/renderer/src/libs/core/getNotesAPI.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import NotesAPI from '@/libs/core/NotesAPI.js'
|
||||||
|
import IpcAdapter from '@/libs/core/IpcAdapter.js'
|
||||||
|
import useConfig from '@/composables/useConfig.js'
|
||||||
|
|
||||||
|
// Singleton pattern to make sure only one instance of NotesAPI exists
|
||||||
|
let notesAPI = null
|
||||||
|
let initPromise = null
|
||||||
|
|
||||||
|
const generateEncryptionKey = () => {
|
||||||
|
const array = new Uint8Array(32)
|
||||||
|
crypto.getRandomValues(array)
|
||||||
|
return Array.from(array)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const createInstance = async () => {
|
||||||
|
const { config, ensureConfig } = useConfig()
|
||||||
|
await ensureConfig()
|
||||||
|
|
||||||
|
let encryptionKey = config.value?.encryptionKey
|
||||||
|
|
||||||
|
if (!encryptionKey) {
|
||||||
|
encryptionKey = generateEncryptionKey()
|
||||||
|
config.value.encryptionKey = encryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = new IpcAdapter()
|
||||||
|
const api = new NotesAPI(adapter, encryptionKey)
|
||||||
|
|
||||||
|
await api.init()
|
||||||
|
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNotesAPI = async () => {
|
||||||
|
if (notesAPI) return notesAPI
|
||||||
|
|
||||||
|
if (!initPromise) {
|
||||||
|
initPromise = createInstance().then((api) => {
|
||||||
|
notesAPI = api
|
||||||
|
return api
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return initPromise
|
||||||
|
}
|
||||||
@@ -4,43 +4,27 @@ const colors = {
|
|||||||
'grey-100': '#747474',
|
'grey-100': '#747474',
|
||||||
green: '#87FF5B',
|
green: '#87FF5B',
|
||||||
blue: '#5B92FF',
|
blue: '#5B92FF',
|
||||||
|
purple: '#94079E',
|
||||||
}
|
}
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
light: {
|
|
||||||
bg: colors.white,
|
|
||||||
fg: colors.black,
|
|
||||||
accent: colors.green,
|
|
||||||
link: colors.blue,
|
|
||||||
},
|
|
||||||
dark: {
|
dark: {
|
||||||
bg: colors.black,
|
bg: colors.black,
|
||||||
fg: colors.white,
|
fg: colors.white,
|
||||||
accent: colors.green,
|
accent: colors.green,
|
||||||
link: colors.blue,
|
link: colors.blue,
|
||||||
},
|
},
|
||||||
}
|
light: {
|
||||||
|
bg: colors.white,
|
||||||
const breakpoints = {
|
fg: colors.black,
|
||||||
mobile: 800,
|
accent: colors.purple,
|
||||||
}
|
link: colors.blue,
|
||||||
|
|
||||||
const viewports = {
|
|
||||||
mobile: {
|
|
||||||
width: 200,
|
|
||||||
height: 956,
|
|
||||||
},
|
|
||||||
desktop: {
|
|
||||||
width: 354,
|
|
||||||
height: 549,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export { colors, themes, breakpoints, viewports }
|
export { colors, themes }
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
colors,
|
colors,
|
||||||
themes,
|
themes,
|
||||||
breakpoints,
|
|
||||||
viewports,
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/renderer/src/libs/uint.js
Normal file
35
src/renderer/src/libs/uint.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const hexToUint8Array = (hex) => {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2)
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const base64ToUint8Array = (base64) => {
|
||||||
|
const binary = atob(base64)
|
||||||
|
const bytes = new Uint8Array(binary.length)
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uint8ArrayToBase64 = (bytes) => {
|
||||||
|
let binary = ''
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i])
|
||||||
|
}
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const concatUint8Arrays = (...arrays) => {
|
||||||
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
|
||||||
|
const result = new Uint8Array(totalLength)
|
||||||
|
let offset = 0
|
||||||
|
for (const arr of arrays) {
|
||||||
|
result.set(arr, offset)
|
||||||
|
offset += arr.length
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
import Directory from '@/views/Directory.vue'
|
import Directory from '@/views/Directory.vue'
|
||||||
import Editor from '@/views/Editor.vue'
|
import Note from '@/views/Note.vue'
|
||||||
|
import CreateCategory from '@/views/CreateCategory.vue'
|
||||||
import Category from '@/views/Category.vue'
|
import Category from '@/views/Category.vue'
|
||||||
import Instructions from '@/views/Instructions.vue'
|
import Instructions from '@/views/Instructions.vue'
|
||||||
|
import Search from '@/views/Search.vue'
|
||||||
|
import Preferences from '@/views/Preferences.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'directory', component: Directory },
|
{ path: '/', name: 'directory', component: Directory },
|
||||||
{ path: '/note/:id', name: 'note', component: Editor },
|
{ path: '/note/:id', name: 'note', component: Note },
|
||||||
|
{ path: '/category', name: 'create-category', component: CreateCategory },
|
||||||
{ path: '/category/:id', name: 'category', component: Category },
|
{ path: '/category/:id', name: 'category', component: Category },
|
||||||
{ path: '/instructions', name: 'instructions', component: Instructions },
|
{ path: '/instructions', name: 'instructions', component: Instructions },
|
||||||
|
{ path: '/search', name: 'search', component: Search },
|
||||||
|
{ path: '/preferences', name: 'preferences', component: Preferences },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHashHistory(),
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-size: size-vw(30px);
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin h1-mono {
|
@mixin h1-mono {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: size-vw(22px);
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin p {
|
@mixin p {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: size-vw(12px);
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src:
|
src:
|
||||||
url('../../../../resources/fonts/leibniz-fraktur.woff2') format('woff2'),
|
url('../../../../resources/fonts/neuefraktur.woff2') format('woff2'),
|
||||||
url('../../../../resources/fonts/leibniz-fraktur.woff') format('woff');
|
url('../../../../resources/fonts/neuefraktur.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Geist Mono */
|
/* Geist Mono */
|
||||||
|
|||||||
@@ -1,58 +1,3 @@
|
|||||||
@use 'sass:math';
|
|
||||||
|
|
||||||
/* Breakpoints */
|
|
||||||
$mobile-breakpoint: get('breakpoints.mobile');
|
|
||||||
|
|
||||||
// Viewport Sizes
|
|
||||||
$desktop-width: get('viewports.desktop.width');
|
|
||||||
$desktop-height: get('viewports.desktop.height');
|
|
||||||
|
|
||||||
$mobile-width: get('viewports.mobile.width');
|
|
||||||
$mobile-height: get('viewports.mobile.height');
|
|
||||||
|
|
||||||
// Breakpoint
|
|
||||||
@mixin mobile {
|
|
||||||
@media (max-width: #{$mobile-breakpoint * 1px - 1px}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin desktop {
|
|
||||||
@media (min-width: #{$mobile-breakpoint * 1px}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@function mobile-vw($pixels, $base-vw: $mobile-width) {
|
|
||||||
$px: math.div($pixels, $base-vw);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function mobile-vh($pixels, $base-vh: $mobile-height) {
|
|
||||||
$px: math.div($pixels, $base-vh);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function desktop-vw($pixels, $base-vw: $desktop-width) {
|
|
||||||
$px: math.div($pixels, $base-vw);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function desktop-vh($pixels, $base-vh: $desktop-height) {
|
|
||||||
$px: math.div($pixels, $base-vh);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function size-vw($pixels, $base-vw: 354) {
|
|
||||||
$px: math.div($pixels, $base-vw);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function columns($columns) {
|
@function columns($columns) {
|
||||||
@return calc(
|
@return calc(
|
||||||
(#{$columns} * var(--layout-column-width)) +
|
(#{$columns} * var(--layout-column-width)) +
|
||||||
@@ -72,14 +17,6 @@ $mobile-height: get('viewports.mobile.height');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin fill($position: absolute) {
|
|
||||||
position: #{$position};
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin fade-on-ready($class: 'ready', $duration: 400ms) {
|
@mixin fade-on-ready($class: 'ready', $duration: 400ms) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity $duration ease;
|
transition: opacity $duration ease;
|
||||||
@@ -90,90 +27,12 @@ $mobile-height: get('viewports.mobile.height');
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clamp text block to number of lines
|
// Clamp text block to number of lines
|
||||||
@mixin line-clamp($lines: 3, $mobile-lines: $lines) {
|
@mixin line-clamp($lines: 3) {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: $lines;
|
-webkit-line-clamp: $lines;
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
-webkit-line-clamp: $mobile-lines;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flip animations
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin flip-animation(
|
|
||||||
$direction: 'r',
|
|
||||||
$duration: 600ms,
|
|
||||||
$easing: var(--ease-out-expo),
|
|
||||||
$iteration-count: 1
|
|
||||||
) {
|
|
||||||
overflow: hidden;
|
|
||||||
animation: flip-#{$direction} $duration $easing $iteration-count forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin link-hover {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--theme-fg);
|
|
||||||
transform-origin: left;
|
|
||||||
transform: scaleX(0);
|
|
||||||
transition: transform 300ms var(--ease-out-quad);
|
|
||||||
}
|
|
||||||
@include desktop {
|
|
||||||
&:hover::before {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin stagger-animate($stagger: 100, $num-children: 10, $base-delay: 0) {
|
@mixin stagger-animate($stagger: 100, $num-children: 10, $base-delay: 0) {
|
||||||
@@ -185,20 +44,18 @@ $mobile-height: get('viewports.mobile.height');
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin hover {
|
@mixin hover {
|
||||||
@include desktop {
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@mixin drop-cap() {
|
@mixin drop-cap() {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: size-vw(12px);
|
font-size: 12px;
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
|
|
||||||
&:first-child::first-letter {
|
&:first-child::first-letter {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: size-vw(42px);
|
font-size: 42px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@
|
|||||||
|
|
||||||
// css classes exposed globally:
|
// css classes exposed globally:
|
||||||
// .layout-block: element takes the whole layout width
|
// .layout-block: element takes the whole layout width
|
||||||
// .layout-block-inner: same as .layout-block but using padding instead of margin
|
|
||||||
// .layout-grid: extends .layout-block with grid behaviour using layout settings
|
// .layout-grid: extends .layout-block with grid behaviour using layout settings
|
||||||
// .layout-grid-inner: same as .layout-grid but using padding instead of margin
|
|
||||||
|
|
||||||
@use 'sass:map';
|
@use 'sass:map';
|
||||||
|
|
||||||
@@ -20,26 +18,21 @@
|
|||||||
// 'variable': (mobile, desktop)
|
// 'variable': (mobile, desktop)
|
||||||
$layout: (
|
$layout: (
|
||||||
'columns-count': (
|
'columns-count': (
|
||||||
5,
|
6,
|
||||||
18,
|
|
||||||
),
|
),
|
||||||
'columns-gap': (
|
'columns-gap': (
|
||||||
20px,
|
10px,
|
||||||
20px,
|
|
||||||
),
|
),
|
||||||
'margin': (
|
'margin': (
|
||||||
30px,
|
20px,
|
||||||
60px,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
//internal process, do not touch
|
//internal process, do not touch
|
||||||
:root {
|
:root {
|
||||||
--layout-column-count: #{list.nth(map.get($layout, 'columns-count'), 1)};
|
--layout-column-count: #{list.nth(map.get($layout, 'columns-count'), 1)};
|
||||||
--layout-column-gap: #{size-vw(
|
--layout-column-gap: #{list.nth(map.get($layout, 'columns-gap'), 1)};
|
||||||
list.nth(map.get($layout, 'columns-gap'), 1)
|
--layout-margin: #{list.nth(map.get($layout, 'margin'), 1)};
|
||||||
)};
|
|
||||||
--layout-margin: #{size-vw(list.nth(map.get($layout, 'margin'), 1))};
|
|
||||||
--layout-width: calc(100vw - (2 * var(--layout-margin)));
|
--layout-width: calc(100vw - (2 * var(--layout-margin)));
|
||||||
--layout-column-width: calc(
|
--layout-column-width: calc(
|
||||||
(
|
(
|
||||||
@@ -54,13 +47,6 @@ $layout: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layout-block {
|
.layout-block {
|
||||||
max-width: var(--layout-width);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-block-inner {
|
|
||||||
padding-left: var(--layout-margin);
|
padding-left: var(--layout-margin);
|
||||||
padding-right: var(--layout-margin);
|
padding-right: var(--layout-margin);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -73,11 +59,3 @@ $layout: (
|
|||||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
||||||
grid-gap: var(--layout-column-gap);
|
grid-gap: var(--layout-column-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-grid-inner {
|
|
||||||
@extend .layout-block-inner;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
|
||||||
grid-gap: var(--layout-column-gap);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,23 +10,3 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.lenis {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-smooth {
|
|
||||||
scroll-behavior: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-stopped {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-scrolling iframe {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|||||||
52
src/renderer/src/styles/_syntax.scss
Normal file
52
src/renderer/src/styles/_syntax.scss
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* Code styling */
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: var(--grey-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #ff5b69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-params {
|
||||||
|
color: #ffa55b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section {
|
||||||
|
color: #fcff5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #5b9aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
@@ -17,26 +17,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-only {
|
|
||||||
@include desktop {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-only {
|
|
||||||
@include mobile {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.has-scroll-smooth) {
|
html:not(.has-scroll-smooth) {
|
||||||
.hide-on-native-scroll {
|
.hide-on-native-scroll {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.has-scroll-smooth {
|
|
||||||
.hide-on-smooth-scroll {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
@use 'font-style' as *;
|
@use 'font-style' as *;
|
||||||
@use 'layout' as *;
|
@use 'layout' as *;
|
||||||
@use 'scroll' as *;
|
@use 'scroll' as *;
|
||||||
|
@use 'syntax' as *;
|
||||||
@use 'transitions' as *;
|
@use 'transitions' as *;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -46,7 +47,10 @@ p,
|
|||||||
a,
|
a,
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
pre {
|
pre,
|
||||||
|
span,
|
||||||
|
label,
|
||||||
|
li {
|
||||||
@include p;
|
@include p;
|
||||||
}
|
}
|
||||||
.bold {
|
.bold {
|
||||||
@@ -64,9 +68,9 @@ button {
|
|||||||
// Text selection
|
// Text selection
|
||||||
::selection {
|
::selection {
|
||||||
color: var(--theme-bg);
|
color: var(--theme-bg);
|
||||||
background: var(--theme-accent);
|
background: var(--theme-fg);
|
||||||
}
|
}
|
||||||
::-moz-selection {
|
::-moz-selection {
|
||||||
color: var(--theme-bg);
|
color: var(--theme-bg);
|
||||||
background: var(--theme-accent);
|
background: var(--theme-fg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="category layout-block">
|
<main class="category layout-block">
|
||||||
<router-link class="back" to="/"><- Go Back</router-link>
|
<category-row
|
||||||
|
:index="categoryIndex"
|
||||||
<category-row :index="categoryIndex" :category="id" />
|
:category="id"
|
||||||
|
editable
|
||||||
|
@edited="onCategoryEdited"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="notes">
|
<div class="notes">
|
||||||
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
||||||
@@ -13,48 +16,63 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import useNotes from '@/composables/useNotes'
|
import useNotes from '@/composables/useNotes'
|
||||||
import NoteRow from '@/components/NoteRow.vue'
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
import CategoryRow from '@/components/CategoryRow.vue'
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
import NewNote from '@/components/NewNote.vue'
|
import NewNote from '@/components/NewNote.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const id = route.params.id
|
const id = route.params?.id
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const { categories, loadCategoryNotes } = useNotes()
|
const { categories, loadCategoryNotes, updateCategory, notesChangeCount } =
|
||||||
|
useNotes()
|
||||||
|
|
||||||
const notes = ref()
|
const notes = ref()
|
||||||
|
|
||||||
onMounted(async () => {
|
async function refreshNotes() {
|
||||||
|
if (id) {
|
||||||
notes.value = await loadCategoryNotes(id)
|
notes.value = await loadCategoryNotes(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(notesChangeCount, async () => {
|
||||||
|
await refreshNotes()
|
||||||
|
})
|
||||||
|
|
||||||
|
const onCategoryEdited = async (editedCategory) => {
|
||||||
|
await updateCategory(id, editedCategory)
|
||||||
|
|
||||||
|
router.push({ name: 'category', params: { id: editedCategory } })
|
||||||
|
}
|
||||||
|
|
||||||
const categoryIndex = computed(() => {
|
const categoryIndex = computed(() => {
|
||||||
return categories.value?.findIndex((category) => category === id) || 0
|
return categories.value?.findIndex((category) => category === id) || 1
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
main.category {
|
main.category {
|
||||||
.back {
|
padding-top: var(--nav-height);
|
||||||
display: block;
|
|
||||||
opacity: 0.25;
|
|
||||||
margin-top: size-vw(9px);
|
|
||||||
}
|
|
||||||
.category-row {
|
.category-row {
|
||||||
margin-top: size-vw(4px);
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.notes {
|
.notes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: size-vw(14px);
|
gap: 14px;
|
||||||
margin-top: size-vw(9px);
|
margin-top: 9px;
|
||||||
}
|
}
|
||||||
.new-note {
|
.new-note {
|
||||||
display: block;
|
display: block;
|
||||||
margin: size-vw(50px) auto 0;
|
margin: 50px auto 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
22
src/renderer/src/views/CreateCategory.vue
Normal file
22
src/renderer/src/views/CreateCategory.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<main class="create-category layout-block">
|
||||||
|
<category-row :index="1" editable @edited="onCategoryEdited" />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const onCategoryEdited = (name) => {
|
||||||
|
router.push({ name: 'category', params: { id: name } })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.create-category {
|
||||||
|
padding-top: var(--nav-height);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="directory layout-block">
|
<main v-if="loaded" class="directory layout-block">
|
||||||
<category-row
|
<category-row
|
||||||
v-for="(category, i) in categories"
|
v-for="(category, i) in categories"
|
||||||
:index="i"
|
:index="i"
|
||||||
@@ -7,43 +7,63 @@
|
|||||||
:key="category"
|
:key="category"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="label">Summarium</h2>
|
<h2 v-if="notes?.length" class="label">Summarium</h2>
|
||||||
|
|
||||||
<div class="notes">
|
<div class="notes">
|
||||||
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<page-loading v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useNotes from '@/composables/useNotes'
|
import useNotes from '@/composables/useNotes'
|
||||||
import { onMounted, ref } from 'vue'
|
import usePlugins from '@/composables/usePlugins'
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import CategoryRow from '@/components/CategoryRow.vue'
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
import NoteRow from '@/components/NoteRow.vue'
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
|
import PageLoading from '@/components/PageLoading.vue'
|
||||||
|
|
||||||
const { categories, loadCategories, loadCategoryNotes } = useNotes()
|
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
|
||||||
|
useNotes()
|
||||||
|
|
||||||
|
const { config } = useConfig()
|
||||||
|
|
||||||
const notes = ref()
|
const notes = ref()
|
||||||
|
const loaded = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
const refreshNotes = async () => {
|
||||||
|
loaded.value = false
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
notes.value = await loadCategoryNotes()
|
notes.value = await loadCategoryNotes()
|
||||||
|
loaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshNotes()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(notesChangeCount, async () => {
|
||||||
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
main.directory {
|
main.directory {
|
||||||
padding-top: size-vw(18px);
|
padding-top: var(--nav-height);
|
||||||
|
padding-bottom: 30px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: size-vw(17px) 0 size-vw(24px);
|
margin: 17px 0 24px;
|
||||||
@include p;
|
@include p;
|
||||||
}
|
}
|
||||||
.notes {
|
.notes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: size-vw(14px);
|
gap: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main v-if="editor" class="editor layout-block">
|
|
||||||
<bubble-menu :editor="editor">
|
|
||||||
<div class="bubble-menu">
|
|
||||||
<button
|
|
||||||
@click="editor.chain().focus().toggleBold().run()"
|
|
||||||
:class="{ active: editor.isActive('bold') }"
|
|
||||||
>
|
|
||||||
Bold
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="editor.chain().focus().toggleItalic().run()"
|
|
||||||
:class="{ active: editor.isActive('italic') }"
|
|
||||||
>
|
|
||||||
Italic
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</bubble-menu>
|
|
||||||
|
|
||||||
<editor-content :editor="editor" class="editor-wrap" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
|
||||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
||||||
import { Markdown } from '@tiptap/markdown'
|
|
||||||
import Image from '@tiptap/extension-image'
|
|
||||||
import Document from '@tiptap/extension-document'
|
|
||||||
import { Placeholder } from '@tiptap/extensions'
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
import { BubbleMenu } from '@tiptap/vue-3/menus'
|
|
||||||
import useNotes from '@/composables/useNotes'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import _debounce from 'lodash/debounce'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const id = route.params.id
|
|
||||||
|
|
||||||
const { loadNote, updateNoteContent, updateNoteMetadata } = useNotes()
|
|
||||||
|
|
||||||
const CustomDocument = Document.extend({
|
|
||||||
content: 'heading block*',
|
|
||||||
})
|
|
||||||
|
|
||||||
const editor = shallowRef()
|
|
||||||
|
|
||||||
const updateNote = _debounce(async ({ editor }) => {
|
|
||||||
const markdown = editor.getMarkdown()
|
|
||||||
|
|
||||||
await updateNoteContent(id, markdown)
|
|
||||||
|
|
||||||
updateTitle(editor)
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
// Track title updates for file
|
|
||||||
let lastTitle
|
|
||||||
const updateTitle = _debounce(async (editor) => {
|
|
||||||
const doc = editor.state.doc
|
|
||||||
|
|
||||||
const firstNode = doc.firstChild
|
|
||||||
if (!firstNode || firstNode.type.name !== 'heading') return
|
|
||||||
|
|
||||||
const newTitle = firstNode.textContent.trim() || 'Untitled'
|
|
||||||
|
|
||||||
if (newTitle === lastTitle) return
|
|
||||||
lastTitle = newTitle
|
|
||||||
|
|
||||||
await updateNoteMetadata(id, { title: newTitle })
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const note = await loadNote(id)
|
|
||||||
lastTitle = note.title
|
|
||||||
|
|
||||||
editor.value = new Editor({
|
|
||||||
extensions: [
|
|
||||||
CustomDocument,
|
|
||||||
StarterKit.configure({
|
|
||||||
document: false,
|
|
||||||
heading: { levels: [1] },
|
|
||||||
trailingNode: {
|
|
||||||
node: 'paragraph',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Placeholder.configure({
|
|
||||||
placeholder: ({ node }) => {
|
|
||||||
if (node.type.name === 'heading') {
|
|
||||||
return 'Title'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Markdown.configure({}),
|
|
||||||
Image,
|
|
||||||
],
|
|
||||||
content: note.content,
|
|
||||||
contentType: 'markdown',
|
|
||||||
onUpdate: updateNote,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
editor.value?.destroy?.()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
main.editor {
|
|
||||||
padding-top: size-vw(8px);
|
|
||||||
padding-bottom: size-vw(20px);
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 700 !important;
|
|
||||||
@include p;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
@include drop-cap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h1.is-editor-empty:first-child::before {
|
|
||||||
color: var(--grey-100);
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
pointer-events: none;
|
|
||||||
@include drop-cap;
|
|
||||||
}
|
|
||||||
p strong {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
p em {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
hr::before {
|
|
||||||
content: '----------------------------------------';
|
|
||||||
@include p;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: list-item;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: list-item;
|
|
||||||
margin-left: 1.5em;
|
|
||||||
|
|
||||||
&::marker {
|
|
||||||
@include p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--theme-link);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-wrap {
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: size-vw(20px);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-menu {
|
|
||||||
display: flex;
|
|
||||||
gap: size-vw(5px);
|
|
||||||
border: 1px solid var(--grey-100);
|
|
||||||
color: var(--grey-100);
|
|
||||||
border-radius: 0.2em;
|
|
||||||
background: var(--theme-bg);
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.2em;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--grey-100);
|
|
||||||
color: var(--theme-bg);
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background: var(--theme-fg);
|
|
||||||
color: var(--theme-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,74 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="instructions layout-block-inner">
|
<main class="instructions layout-block">
|
||||||
<router-link class="back-link" to="/"><- Go Back</router-link>
|
<div class="content" v-html="renderedContent" />
|
||||||
|
|
||||||
<p>
|
|
||||||
Medieval Translation Nota = Note Capitulum = Category Intructio =
|
|
||||||
Instructions Tabula = Index/Overview *This can be disabled via
|
|
||||||
toolbar -------------------------------------------- Program Key
|
|
||||||
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
|
|
||||||
cmd + x = close window dbl click = change name / open nota paste
|
|
||||||
hyperlink twice = activated url
|
|
||||||
-------------------------------------------- Text Markdowns cmd + b
|
|
||||||
= Bold cmd + u = underline --- = ---------- (ruled line break)
|
|
||||||
/*text*/ = Desaturated text
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Medieval Translation Nota = Note Capitulum = Category Intructio =
|
|
||||||
Instructions Tabula = Index/Overview *This can be disabled via
|
|
||||||
toolbar -------------------------------------------- Program Key
|
|
||||||
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
|
|
||||||
cmd + x = close window dbl click = change name / open nota paste
|
|
||||||
hyperlink twice = activated url
|
|
||||||
-------------------------------------------- Text Markdowns cmd + b
|
|
||||||
= Bold cmd + u = underline --- = ---------- (ruled line break)
|
|
||||||
/*text*/ = Desaturated text
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Medieval Translation Nota = Note Capitulum = Category Intructio =
|
|
||||||
Instructions Tabula = Index/Overview *This can be disabled via
|
|
||||||
toolbar -------------------------------------------- Program Key
|
|
||||||
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
|
|
||||||
cmd + x = close window dbl click = change name / open nota paste
|
|
||||||
hyperlink twice = activated url
|
|
||||||
-------------------------------------------- Text Markdowns cmd + b
|
|
||||||
= Bold cmd + u = underline --- = ---------- (ruled line break)
|
|
||||||
/*text*/ = Desaturated text
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Medieval Translation Nota = Note Capitulum = Category Intructio =
|
|
||||||
Instructions Tabula = Index/Overview *This can be disabled via
|
|
||||||
toolbar -------------------------------------------- Program Key
|
|
||||||
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
|
|
||||||
cmd + x = close window dbl click = change name / open nota paste
|
|
||||||
hyperlink twice = activated url
|
|
||||||
-------------------------------------------- Text Markdowns cmd + b
|
|
||||||
= Bold cmd + u = underline --- = ---------- (ruled line break)
|
|
||||||
/*text*/ = Desaturated text
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Medieval Translation Nota = Note Capitulum = Category Intructio =
|
|
||||||
Instructions Tabula = Index/Overview *This can be disabled via
|
|
||||||
toolbar -------------------------------------------- Program Key
|
|
||||||
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
|
|
||||||
cmd + x = close window dbl click = change name / open nota paste
|
|
||||||
hyperlink twice = activated url
|
|
||||||
-------------------------------------------- Text Markdowns cmd + b
|
|
||||||
= Bold cmd + u = underline --- = ---------- (ruled line break)
|
|
||||||
/*text*/ = Desaturated text
|
|
||||||
</p>
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script setup>
|
||||||
|
import content from '@/content/instructions.md?raw'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
const md = new MarkdownIt()
|
||||||
|
const renderedContent = md.render(content)
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
main.instructions {
|
main.instructions {
|
||||||
.back-link {
|
padding-top: var(--nav-height);
|
||||||
opacity: 0.25;
|
|
||||||
display: block;
|
.content {
|
||||||
margin-top: size-vw(9px);
|
display: flex;
|
||||||
margin-bottom: size-vw(14px);
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-bottom: 1px dashed currentColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
41
src/renderer/src/views/Note.vue
Normal file
41
src/renderer/src/views/Note.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<main class="note layout-block">
|
||||||
|
<note-download :editor="editorRef?.editor" />
|
||||||
|
|
||||||
|
<note-find
|
||||||
|
:editor="editorRef?.editor"
|
||||||
|
:visible="findVisible"
|
||||||
|
@close="findVisible = false"
|
||||||
|
/>
|
||||||
|
<note-editor ref="editorRef" :id="id" />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watchEffect } from 'vue'
|
||||||
|
import { useMagicKeys } from '@vueuse/core'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import NoteEditor from '@/components/note/Editor.vue'
|
||||||
|
import NoteFind from '@/components/note/Find.vue'
|
||||||
|
import NoteDownload from '@/components/note/Download.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const id = route.params.id
|
||||||
|
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const findVisible = ref(false)
|
||||||
|
|
||||||
|
const { ctrl, f } = useMagicKeys()
|
||||||
|
watchEffect(() => {
|
||||||
|
if (ctrl.value && f.value) {
|
||||||
|
findVisible.value = !findVisible.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main.note {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
189
src/renderer/src/views/Preferences.vue
Normal file
189
src/renderer/src/views/Preferences.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<main class="preferences layout-block">
|
||||||
|
<h1 class="mono">Storage Plugin</h1>
|
||||||
|
|
||||||
|
<div v-for="plugin in plugins" class="plugin" :key="plugin.id">
|
||||||
|
<input
|
||||||
|
v-model="selectedPluginId"
|
||||||
|
name="plugins"
|
||||||
|
type="radio"
|
||||||
|
:id="plugin.id"
|
||||||
|
:value="plugin.id"
|
||||||
|
/>
|
||||||
|
<div class="info">
|
||||||
|
<p class="name bold">{{ plugin.name }}</p>
|
||||||
|
<p class="description">{{ plugin.description }}</p>
|
||||||
|
|
||||||
|
<div v-if="plugin.configSchema.length" class="config">
|
||||||
|
<div
|
||||||
|
v-for="field in plugin.configSchema"
|
||||||
|
class="config-field"
|
||||||
|
:key="field.key"
|
||||||
|
>
|
||||||
|
<label :for="field.key">
|
||||||
|
{{ field.label }} {{ field.required ? '*' : '' }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="config.adapters[plugin.id][field.key]"
|
||||||
|
:id="field.key"
|
||||||
|
:type="field.type"
|
||||||
|
:placeholder="field.default"
|
||||||
|
:required="field.required"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="validationError" class="error">{{ validationError }}</p>
|
||||||
|
|
||||||
|
<button @click="save" class="save-btn">
|
||||||
|
<svg-spinner v-if="saving" />
|
||||||
|
<span v-else-if="saved">Saved</span>
|
||||||
|
<span v-else>Save</span>
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgSpinner from '@/components/svg/Spinner.vue'
|
||||||
|
import usePlugins from '@/composables/usePlugins'
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const { plugins, setActivePlugin } = await usePlugins()
|
||||||
|
const { config, ensureConfig } = useConfig()
|
||||||
|
await ensureConfig()
|
||||||
|
|
||||||
|
const normalizeConfig = () => {
|
||||||
|
if (!config.value.adapters) {
|
||||||
|
config.value.adapters = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const plugin of plugins.value) {
|
||||||
|
if (!config.value.adapters[plugin.id]) {
|
||||||
|
config.value.adapters[plugin.id] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of plugin.configSchema) {
|
||||||
|
if (config.value.adapters[plugin.id][field.key] === undefined) {
|
||||||
|
config.value.adapters[plugin.id][field.key] =
|
||||||
|
field.default ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizeConfig()
|
||||||
|
|
||||||
|
const selectedPluginId = ref(config.value.activeAdapter)
|
||||||
|
const validationError = ref('')
|
||||||
|
|
||||||
|
const selectedPlugin = computed(() => {
|
||||||
|
return plugins.value.find((p) => p.id === selectedPluginId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const saving = ref(false)
|
||||||
|
const saved = ref(false)
|
||||||
|
const save = async () => {
|
||||||
|
saving.value = true
|
||||||
|
validationError.value = ''
|
||||||
|
|
||||||
|
const plugin = selectedPlugin.value
|
||||||
|
if (plugin && plugin.configSchema.length) {
|
||||||
|
const adapterConfig = config.value.adapters[plugin.id] || {}
|
||||||
|
for (const field of plugin.configSchema) {
|
||||||
|
if (field.required && !adapterConfig[field.key]) {
|
||||||
|
validationError.value = `Please fill in all required fields for ${plugin.name}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await setActivePlugin(selectedPluginId.value)
|
||||||
|
saving.value = false
|
||||||
|
saved.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
saved.value = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.preferences {
|
||||||
|
padding-top: var(--nav-height);
|
||||||
|
padding-bottom: 60px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='radio'] {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 1px solid var(--theme-fg);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background-color: var(--theme-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
.description {
|
||||||
|
color: var(--grey-100);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.config-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px dashed currentColor;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
|
||||||
|
.svg-spinner {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
62
src/renderer/src/views/Search.vue
Normal file
62
src/renderer/src/views/Search.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<main class="search layout-block">
|
||||||
|
<form @submit.prevent="onSearch">
|
||||||
|
<search-input
|
||||||
|
v-model="query"
|
||||||
|
placeholder="Search"
|
||||||
|
ref="searchInput"
|
||||||
|
@input="onInput"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
<note-row
|
||||||
|
v-for="note in searchResults"
|
||||||
|
:key="note.id"
|
||||||
|
:note="note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import SearchInput from '@/components/SearchInput.vue'
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
|
import _debounce from 'lodash/debounce'
|
||||||
|
|
||||||
|
const query = ref('')
|
||||||
|
const searchInput = ref()
|
||||||
|
|
||||||
|
const { search, searchResults } = useNotes()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
searchInput.value?.focus()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
query.value = ''
|
||||||
|
searchResults.value = []
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSearch = async () => {
|
||||||
|
await search(query.value)
|
||||||
|
}
|
||||||
|
const onInput = _debounce(async () => {
|
||||||
|
await search(query.value)
|
||||||
|
}, 300)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main.search {
|
||||||
|
padding-top: var(--nav-height);
|
||||||
|
|
||||||
|
.results {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user