Improve mobile layout and configure Prettier for Svelte
This commit is contained in:
parent
8f67269f44
commit
c2a94ba6f4
8
Journal.App/.prettierignore
Normal file
8
Journal.App/.prettierignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
.svelte-kit/
|
||||||
|
.vscode/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
target/
|
||||||
|
src-tauri/target/
|
||||||
@ -18,12 +18,12 @@ npm run tauri dev # Tauri desktop window (connects to dev server)
|
|||||||
|
|
||||||
## Build Targets
|
## Build Targets
|
||||||
|
|
||||||
| Command | Output | Use case |
|
| Command | Output | Use case |
|
||||||
|---------|--------|----------|
|
| ------------------------------------------------------------ | ----------------------------------------- | ----------------------------------- |
|
||||||
| `npm run build` | `Journal.App/build/` | Web bundle for `Journal.WebGateway` |
|
| `npm run build` | `Journal.App/build/` | Web bundle for `Journal.WebGateway` |
|
||||||
| `.\scripts\publish-app.ps1 -Target web` | `Journal.App/build/` | Same, via script |
|
| `.\scripts\publish-app.ps1 -Target web` | `Journal.App/build/` | Same, via script |
|
||||||
| `.\scripts\publish-app.ps1 -Target tauri -TauriBundles none` | `src-tauri/target/release/journalapp.exe` | Raw desktop exe |
|
| `.\scripts\publish-app.ps1 -Target tauri -TauriBundles none` | `src-tauri/target/release/journalapp.exe` | Raw desktop exe |
|
||||||
| `.\scripts\publish-app.ps1 -Target tauri -TauriBundles nsis` | NSIS installer | Packaged installer |
|
| `.\scripts\publish-app.ps1 -Target tauri -TauriBundles nsis` | NSIS installer | Packaged installer |
|
||||||
|
|
||||||
## Frontend State Management
|
## Frontend State Management
|
||||||
|
|
||||||
@ -31,13 +31,13 @@ Svelte stores are the source of truth for all feature state.
|
|||||||
|
|
||||||
### Current Stores
|
### Current Stores
|
||||||
|
|
||||||
| Store file | State exports | Notes |
|
| Store file | State exports | Notes |
|
||||||
|-----------|---------------|-------|
|
| ----------------------------- | --------------------------------------- | ------------------------------------------------- |
|
||||||
| `src/lib/stores/entries.ts` | `entriesStore` | Entry list, `getDefaultEntry`, `createEntryDraft` |
|
| `src/lib/stores/entries.ts` | `entriesStore` | Entry list, `getDefaultEntry`, `createEntryDraft` |
|
||||||
| `src/lib/stores/fragments.ts` | `fragmentsStore` | Fragment CRUD + parse/serialize helpers |
|
| `src/lib/stores/fragments.ts` | `fragmentsStore` | Fragment CRUD + parse/serialize helpers |
|
||||||
| `src/lib/stores/todos.ts` | `todoListsStore`, `todosStore` | Todo list and item CRUD |
|
| `src/lib/stores/todos.ts` | `todoListsStore`, `todosStore` | Todo list and item CRUD |
|
||||||
| `src/lib/stores/lists.ts` | `listsStore` | Generic list CRUD, `createListDraft` |
|
| `src/lib/stores/lists.ts` | `listsStore` | Generic list CRUD, `createListDraft` |
|
||||||
| `src/lib/stores/settings.ts` | `settingsTags`, `settingsFragmentTypes` | Tag/type config |
|
| `src/lib/stores/settings.ts` | `settingsTags`, `settingsFragmentTypes` | Tag/type config |
|
||||||
|
|
||||||
### Store-First Rule
|
### Store-First Rule
|
||||||
|
|
||||||
@ -47,14 +47,14 @@ Svelte stores are the source of truth for all feature state.
|
|||||||
|
|
||||||
## Tauri Commands (Rust → Frontend)
|
## Tauri Commands (Rust → Frontend)
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
| ------------------ | ------------------------------------------------------------------------------------ |
|
||||||
| `sidecar_command` | Forward a `CommandEnvelope` to `Journal.Sidecar` stdin/stdout and return parsed JSON |
|
| `sidecar_command` | Forward a `CommandEnvelope` to `Journal.Sidecar` stdin/stdout and return parsed JSON |
|
||||||
| `get_sidecar_root` | Get currently resolved sidecar root path |
|
| `get_sidecar_root` | Get currently resolved sidecar root path |
|
||||||
| `set_sidecar_root` | Override root path (saved to `settings.json`, restarts sidecar) |
|
| `set_sidecar_root` | Override root path (saved to `settings.json`, restarts sidecar) |
|
||||||
| `get_ui_settings` | Load tag/fragment-type settings |
|
| `get_ui_settings` | Load tag/fragment-type settings |
|
||||||
| `set_ui_settings` | Persist tag/fragment-type settings |
|
| `set_ui_settings` | Persist tag/fragment-type settings |
|
||||||
| `shutdown` | Stop sidecar, exit app |
|
| `shutdown` | Stop sidecar, exit app |
|
||||||
|
|
||||||
## Sidecar Path Resolution
|
## Sidecar Path Resolution
|
||||||
|
|
||||||
|
|||||||
36
Journal.App/package-lock.json
generated
36
Journal.App/package-lock.json
generated
@ -18,6 +18,8 @@
|
|||||||
"@sveltejs/kit": "^2.9.0",
|
"@sveltejs/kit": "^2.9.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.5.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
@ -906,7 +908,6 @@
|
|||||||
"integrity": "sha512-NXsZLvalgI3HrHG6ogoEVzjyV7bSFQNqQeekfU7nNufQFrRyV3EBDfQKEwxx50peu7spZR42JuC1PFhwxuvBrg==",
|
"integrity": "sha512-NXsZLvalgI3HrHG6ogoEVzjyV7bSFQNqQeekfU7nNufQFrRyV3EBDfQKEwxx50peu7spZR42JuC1PFhwxuvBrg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||||
@ -949,7 +950,6 @@
|
|||||||
"integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
|
"integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
|
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
@ -1256,7 +1256,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -1543,7 +1542,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -1580,6 +1578,33 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||||
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier-plugin-svelte": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@ -1690,7 +1715,6 @@
|
|||||||
"integrity": "sha512-pRUBr6j6uQDgBi208gHnGRMykw0Rf2Yr1HmLyRucsvcaYgIUxswJkT93WZJflsmezu5s8Lq+q78EoyLv2yaFCg==",
|
"integrity": "sha512-pRUBr6j6uQDgBi208gHnGRMykw0Rf2Yr1HmLyRucsvcaYgIUxswJkT93WZJflsmezu5s8Lq+q78EoyLv2yaFCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@ -1770,7 +1794,6 @@
|
|||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -1785,7 +1808,6 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|||||||
@ -9,7 +9,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -22,6 +24,8 @@
|
|||||||
"@sveltejs/kit": "^2.9.0",
|
"@sveltejs/kit": "^2.9.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.5.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
|
|||||||
11
Journal.App/prettier.config.cjs
Normal file
11
Journal.App/prettier.config.cjs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ["prettier-plugin-svelte"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: "*.svelte",
|
||||||
|
options: {
|
||||||
|
parser: "svelte",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -3,9 +3,5 @@
|
|||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": ["core:default", "dialog:default", "opener:default"]
|
||||||
"core:default",
|
|
||||||
"dialog:default",
|
|
||||||
"opener:default"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
<link
|
||||||
<link rel="stylesheet" href="style.css">
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Journal</title>
|
<title>Journal</title>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { pickCase } from "./normalize";
|
|||||||
export function hydrateWorkspace(password: string): Promise<unknown> {
|
export function hydrateWorkspace(password: string): Promise<unknown> {
|
||||||
return sendCommand<unknown>({
|
return sendCommand<unknown>({
|
||||||
action: "db.hydrate_workspace",
|
action: "db.hydrate_workspace",
|
||||||
payload: { password }
|
payload: { password },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,17 +24,19 @@ type PersistOptions = {
|
|||||||
keepalive?: boolean;
|
keepalive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getRuntimeConfig(options: PersistOptions = {}): Promise<RuntimeConfig> {
|
async function getRuntimeConfig(
|
||||||
|
options: PersistOptions = {},
|
||||||
|
): Promise<RuntimeConfig> {
|
||||||
const data = await sendCommand<RuntimeConfigRaw>(
|
const data = await sendCommand<RuntimeConfigRaw>(
|
||||||
{
|
{
|
||||||
action: "config.get"
|
action: "config.get",
|
||||||
},
|
},
|
||||||
options
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataDirectory: pickCase(data, "dataDirectory", "DataDirectory", ""),
|
dataDirectory: pickCase(data, "dataDirectory", "DataDirectory", ""),
|
||||||
vaultDirectory: pickCase(data, "vaultDirectory", "VaultDirectory", "")
|
vaultDirectory: pickCase(data, "vaultDirectory", "VaultDirectory", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +47,8 @@ export async function unlockVaultWorkspace(password: string): Promise<void> {
|
|||||||
payload: {
|
payload: {
|
||||||
password,
|
password,
|
||||||
vaultDirectory: config.vaultDirectory,
|
vaultDirectory: config.vaultDirectory,
|
||||||
dataDirectory: config.dataDirectory
|
dataDirectory: config.dataDirectory,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
@ -57,12 +59,15 @@ export async function unlockVaultWorkspace(password: string): Promise<void> {
|
|||||||
action: "db.hydrate_workspace",
|
action: "db.hydrate_workspace",
|
||||||
payload: {
|
payload: {
|
||||||
password,
|
password,
|
||||||
dataDirectory: config.dataDirectory
|
dataDirectory: config.dataDirectory,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function persistAndClearVault(password: string, options: PersistOptions = {}): Promise<void> {
|
export async function persistAndClearVault(
|
||||||
|
password: string,
|
||||||
|
options: PersistOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
const config = await getRuntimeConfig(options);
|
const config = await getRuntimeConfig(options);
|
||||||
|
|
||||||
await sendCommand<boolean>(
|
await sendCommand<boolean>(
|
||||||
@ -71,19 +76,19 @@ export async function persistAndClearVault(password: string, options: PersistOpt
|
|||||||
payload: {
|
payload: {
|
||||||
password,
|
password,
|
||||||
vaultDirectory: config.vaultDirectory,
|
vaultDirectory: config.vaultDirectory,
|
||||||
dataDirectory: config.dataDirectory
|
dataDirectory: config.dataDirectory,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
options
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendCommand<boolean>(
|
await sendCommand<boolean>(
|
||||||
{
|
{
|
||||||
action: "vault.clear_data_directory",
|
action: "vault.clear_data_directory",
|
||||||
payload: {
|
payload: {
|
||||||
dataDirectory: config.dataDirectory
|
dataDirectory: config.dataDirectory,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
options
|
options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,14 +9,17 @@ type SendCommandOptions = {
|
|||||||
keepalive?: boolean;
|
keepalive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendCommand<T>(command: BackendCommand, options: SendCommandOptions = {}): Promise<T> {
|
export async function sendCommand<T>(
|
||||||
|
command: BackendCommand,
|
||||||
|
options: SendCommandOptions = {},
|
||||||
|
): Promise<T> {
|
||||||
const envelope: BackendCommand = {
|
const envelope: BackendCommand = {
|
||||||
...command,
|
...command,
|
||||||
correlationId: command.correlationId ?? newCorrelationId()
|
correlationId: command.correlationId ?? newCorrelationId(),
|
||||||
};
|
};
|
||||||
const response = await invoke<BackendResponse<T>>("sidecar_command", {
|
const response = await invoke<BackendResponse<T>>("sidecar_command", {
|
||||||
command: envelope,
|
command: envelope,
|
||||||
keepalive: options.keepalive === true
|
keepalive: options.keepalive === true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { sendCommand } from "./client";
|
import { sendCommand } from "./client";
|
||||||
import { normalizeFragment, type FragmentDto, type FragmentDtoRaw } from "./fragments";
|
import {
|
||||||
|
normalizeFragment,
|
||||||
|
type FragmentDto,
|
||||||
|
type FragmentDtoRaw,
|
||||||
|
} from "./fragments";
|
||||||
import { pickCase } from "./normalize";
|
import { pickCase } from "./normalize";
|
||||||
|
|
||||||
export type ParsedSectionDto = {
|
export type ParsedSectionDto = {
|
||||||
@ -107,80 +111,126 @@ function normalizeSection(raw: ParsedSectionDtoRaw): ParsedSectionDto {
|
|||||||
return {
|
return {
|
||||||
title: pickCase(raw, "title", "Title", ""),
|
title: pickCase(raw, "title", "Title", ""),
|
||||||
content: pickCase(raw, "content", "Content", [] as string[]),
|
content: pickCase(raw, "content", "Content", [] as string[]),
|
||||||
checkboxes: pickCase(raw, "checkboxes", "Checkboxes", {} as Record<string, boolean>)
|
checkboxes: pickCase(
|
||||||
|
raw,
|
||||||
|
"checkboxes",
|
||||||
|
"Checkboxes",
|
||||||
|
{} as Record<string, boolean>,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeJournalEntry(raw: JournalEntryDtoRaw | undefined): JournalEntryDto {
|
function normalizeJournalEntry(
|
||||||
const fragments = pickCase(raw, "fragments", "Fragments", [] as FragmentDtoRaw[]);
|
raw: JournalEntryDtoRaw | undefined,
|
||||||
const sections = pickCase(raw, "sections", "Sections", {} as Record<string, ParsedSectionDtoRaw>);
|
): JournalEntryDto {
|
||||||
|
const fragments = pickCase(
|
||||||
|
raw,
|
||||||
|
"fragments",
|
||||||
|
"Fragments",
|
||||||
|
[] as FragmentDtoRaw[],
|
||||||
|
);
|
||||||
|
const sections = pickCase(
|
||||||
|
raw,
|
||||||
|
"sections",
|
||||||
|
"Sections",
|
||||||
|
{} as Record<string, ParsedSectionDtoRaw>,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
date: pickCase(raw, "date", "Date", ""),
|
date: pickCase(raw, "date", "Date", ""),
|
||||||
fragments: fragments.map(normalizeFragment),
|
fragments: fragments.map(normalizeFragment),
|
||||||
rawContent: pickCase(raw, "rawContent", "RawContent", ""),
|
rawContent: pickCase(raw, "rawContent", "RawContent", ""),
|
||||||
sections: Object.fromEntries(
|
sections: Object.fromEntries(
|
||||||
Object.entries(sections).map(([key, value]) => [key, normalizeSection(value)])
|
Object.entries(sections).map(([key, value]) => [
|
||||||
)
|
key,
|
||||||
|
normalizeSection(value),
|
||||||
|
]),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeEntryListItem(raw: EntryListItemDtoRaw): EntryListItemDto {
|
function normalizeEntryListItem(raw: EntryListItemDtoRaw): EntryListItemDto {
|
||||||
return {
|
return {
|
||||||
fileName: pickCase(raw, "fileName", "FileName", ""),
|
fileName: pickCase(raw, "fileName", "FileName", ""),
|
||||||
filePath: pickCase(raw, "filePath", "FilePath", "")
|
filePath: pickCase(raw, "filePath", "FilePath", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeEntryLoadResult(raw: EntryLoadResultDtoRaw): EntryLoadResultDto {
|
function normalizeEntryLoadResult(
|
||||||
const nestedEntry = pickCase(raw, "entry", "Entry", undefined as JournalEntryDtoRaw | undefined);
|
raw: EntryLoadResultDtoRaw,
|
||||||
const entry =
|
): EntryLoadResultDto {
|
||||||
nestedEntry
|
const nestedEntry = pickCase(
|
||||||
? normalizeJournalEntry(nestedEntry)
|
raw,
|
||||||
: normalizeJournalEntry({
|
"entry",
|
||||||
date: pickCase(raw, "date", "Date", undefined as string | undefined),
|
"Entry",
|
||||||
rawContent: pickCase(raw, "rawContent", "RawContent", undefined as string | undefined),
|
undefined as JournalEntryDtoRaw | undefined,
|
||||||
fragments: [],
|
);
|
||||||
sections: {}
|
const entry = nestedEntry
|
||||||
});
|
? normalizeJournalEntry(nestedEntry)
|
||||||
|
: normalizeJournalEntry({
|
||||||
|
date: pickCase(raw, "date", "Date", undefined as string | undefined),
|
||||||
|
rawContent: pickCase(
|
||||||
|
raw,
|
||||||
|
"rawContent",
|
||||||
|
"RawContent",
|
||||||
|
undefined as string | undefined,
|
||||||
|
),
|
||||||
|
fragments: [],
|
||||||
|
sections: {},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileName: pickCase(raw, "fileName", "FileName", ""),
|
fileName: pickCase(raw, "fileName", "FileName", ""),
|
||||||
filePath: pickCase(raw, "filePath", "FilePath", ""),
|
filePath: pickCase(raw, "filePath", "FilePath", ""),
|
||||||
entry
|
entry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeEntrySearchResult(raw: EntrySearchResultDtoRaw): EntrySearchResultDto {
|
function normalizeEntrySearchResult(
|
||||||
const nestedEntry = pickCase(raw, "entry", "Entry", undefined as JournalEntryDtoRaw | undefined);
|
raw: EntrySearchResultDtoRaw,
|
||||||
const entry =
|
): EntrySearchResultDto {
|
||||||
nestedEntry
|
const nestedEntry = pickCase(
|
||||||
? normalizeJournalEntry(nestedEntry)
|
raw,
|
||||||
: normalizeJournalEntry({
|
"entry",
|
||||||
date: pickCase(raw, "date", "Date", undefined as string | undefined),
|
"Entry",
|
||||||
rawContent: pickCase(raw, "rawContent", "RawContent", undefined as string | undefined),
|
undefined as JournalEntryDtoRaw | undefined,
|
||||||
fragments: [],
|
);
|
||||||
sections: {}
|
const entry = nestedEntry
|
||||||
});
|
? normalizeJournalEntry(nestedEntry)
|
||||||
|
: normalizeJournalEntry({
|
||||||
|
date: pickCase(raw, "date", "Date", undefined as string | undefined),
|
||||||
|
rawContent: pickCase(
|
||||||
|
raw,
|
||||||
|
"rawContent",
|
||||||
|
"RawContent",
|
||||||
|
undefined as string | undefined,
|
||||||
|
),
|
||||||
|
fragments: [],
|
||||||
|
sections: {},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileName: pickCase(raw, "fileName", "FileName", ""),
|
fileName: pickCase(raw, "fileName", "FileName", ""),
|
||||||
entry
|
entry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listEntries(dataDirectory?: string): Promise<EntryListItemDto[]> {
|
export async function listEntries(
|
||||||
|
dataDirectory?: string,
|
||||||
|
): Promise<EntryListItemDto[]> {
|
||||||
const data = await sendCommand<EntryListItemDtoRaw[]>({
|
const data = await sendCommand<EntryListItemDtoRaw[]>({
|
||||||
action: "entries.list",
|
action: "entries.list",
|
||||||
payload: { dataDirectory }
|
payload: { dataDirectory },
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.map(normalizeEntryListItem).filter((item) => Boolean(item.filePath));
|
return data
|
||||||
|
.map(normalizeEntryListItem)
|
||||||
|
.filter((item) => Boolean(item.filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadEntry(filePath: string): Promise<EntryLoadResultDto> {
|
export async function loadEntry(filePath: string): Promise<EntryLoadResultDto> {
|
||||||
const data = await sendCommand<EntryLoadResultDtoRaw>({
|
const data = await sendCommand<EntryLoadResultDtoRaw>({
|
||||||
action: "entries.load",
|
action: "entries.load",
|
||||||
payload: { filePath }
|
payload: { filePath },
|
||||||
});
|
});
|
||||||
|
|
||||||
return normalizeEntryLoadResult(data);
|
return normalizeEntryLoadResult(data);
|
||||||
@ -194,26 +244,30 @@ export async function saveEntry(payload: {
|
|||||||
}): Promise<EntrySaveResultDto> {
|
}): Promise<EntrySaveResultDto> {
|
||||||
const data = await sendCommand<EntrySaveResultDtoRaw>({
|
const data = await sendCommand<EntrySaveResultDtoRaw>({
|
||||||
action: "entries.save",
|
action: "entries.save",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath: pickCase(data, "filePath", "FilePath", "")
|
filePath: pickCase(data, "filePath", "FilePath", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEntry(filePath: string): Promise<boolean> {
|
export async function deleteEntry(filePath: string): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "entries.delete",
|
action: "entries.delete",
|
||||||
payload: { filePath }
|
payload: { filePath },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchEntries(payload: EntrySearchRequestDto): Promise<EntrySearchResultDto[]> {
|
export async function searchEntries(
|
||||||
|
payload: EntrySearchRequestDto,
|
||||||
|
): Promise<EntrySearchResultDto[]> {
|
||||||
const data = await sendCommand<EntrySearchResultDtoRaw[]>({
|
const data = await sendCommand<EntrySearchResultDtoRaw[]>({
|
||||||
action: "search.entries",
|
action: "search.entries",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.map(normalizeEntrySearchResult).filter((item) => Boolean(item.fileName));
|
return data
|
||||||
|
.map(normalizeEntrySearchResult)
|
||||||
|
.filter((item) => Boolean(item.fileName));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,13 +41,13 @@ export function normalizeFragment(raw: FragmentDtoRaw): FragmentDto {
|
|||||||
type: pickCase(raw, "type", "Type", ""),
|
type: pickCase(raw, "type", "Type", ""),
|
||||||
description: pickCase(raw, "description", "Description", ""),
|
description: pickCase(raw, "description", "Description", ""),
|
||||||
time: pickCase(raw, "time", "Time", ""),
|
time: pickCase(raw, "time", "Time", ""),
|
||||||
tags: pickCase(raw, "tags", "Tags", [] as string[])
|
tags: pickCase(raw, "tags", "Tags", [] as string[]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listFragments(): Promise<FragmentDto[]> {
|
export async function listFragments(): Promise<FragmentDto[]> {
|
||||||
const data = await sendCommand<FragmentDtoRaw[]>({
|
const data = await sendCommand<FragmentDtoRaw[]>({
|
||||||
action: "fragments.list"
|
action: "fragments.list",
|
||||||
});
|
});
|
||||||
return data.map(normalizeFragment).filter((item) => Boolean(item.id));
|
return data.map(normalizeFragment).filter((item) => Boolean(item.id));
|
||||||
}
|
}
|
||||||
@ -55,32 +55,37 @@ export async function listFragments(): Promise<FragmentDto[]> {
|
|||||||
export async function getFragment(id: string): Promise<FragmentDto | null> {
|
export async function getFragment(id: string): Promise<FragmentDto | null> {
|
||||||
const data = await sendCommand<FragmentDtoRaw | null>({
|
const data = await sendCommand<FragmentDtoRaw | null>({
|
||||||
action: "fragments.get",
|
action: "fragments.get",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const normalized = normalizeFragment(data);
|
const normalized = normalizeFragment(data);
|
||||||
return normalized.id ? normalized : null;
|
return normalized.id ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFragment(payload: CreateFragmentPayload): Promise<FragmentDto> {
|
export async function createFragment(
|
||||||
|
payload: CreateFragmentPayload,
|
||||||
|
): Promise<FragmentDto> {
|
||||||
const data = await sendCommand<FragmentDtoRaw>({
|
const data = await sendCommand<FragmentDtoRaw>({
|
||||||
action: "fragments.create",
|
action: "fragments.create",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
return normalizeFragment(data);
|
return normalizeFragment(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateFragment(id: string, payload: UpdateFragmentPayload): Promise<boolean> {
|
export function updateFragment(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateFragmentPayload,
|
||||||
|
): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "fragments.update",
|
action: "fragments.update",
|
||||||
id,
|
id,
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteFragment(id: string): Promise<boolean> {
|
export function deleteFragment(id: string): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "fragments.delete",
|
action: "fragments.delete",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,13 +38,13 @@ export function normalizeList(raw: ListDocumentDtoRaw): ListDocumentDto {
|
|||||||
label: pickCase(raw, "label", "Label", ""),
|
label: pickCase(raw, "label", "Label", ""),
|
||||||
content: pickCase(raw, "content", "Content", ""),
|
content: pickCase(raw, "content", "Content", ""),
|
||||||
createdAt: pickCase(raw, "createdAt", "CreatedAt", ""),
|
createdAt: pickCase(raw, "createdAt", "CreatedAt", ""),
|
||||||
updatedAt: pickCase(raw, "updatedAt", "UpdatedAt", "")
|
updatedAt: pickCase(raw, "updatedAt", "UpdatedAt", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listLists(): Promise<ListDocumentDto[]> {
|
export async function listLists(): Promise<ListDocumentDto[]> {
|
||||||
const data = await sendCommand<ListDocumentDtoRaw[]>({
|
const data = await sendCommand<ListDocumentDtoRaw[]>({
|
||||||
action: "lists.list"
|
action: "lists.list",
|
||||||
});
|
});
|
||||||
return data.map(normalizeList).filter((item) => Boolean(item.id));
|
return data.map(normalizeList).filter((item) => Boolean(item.id));
|
||||||
}
|
}
|
||||||
@ -52,32 +52,37 @@ export async function listLists(): Promise<ListDocumentDto[]> {
|
|||||||
export async function getList(id: string): Promise<ListDocumentDto | null> {
|
export async function getList(id: string): Promise<ListDocumentDto | null> {
|
||||||
const data = await sendCommand<ListDocumentDtoRaw | null>({
|
const data = await sendCommand<ListDocumentDtoRaw | null>({
|
||||||
action: "lists.get",
|
action: "lists.get",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const normalized = normalizeList(data);
|
const normalized = normalizeList(data);
|
||||||
return normalized.id ? normalized : null;
|
return normalized.id ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createList(payload: CreateListPayload): Promise<ListDocumentDto> {
|
export async function createList(
|
||||||
|
payload: CreateListPayload,
|
||||||
|
): Promise<ListDocumentDto> {
|
||||||
const data = await sendCommand<ListDocumentDtoRaw>({
|
const data = await sendCommand<ListDocumentDtoRaw>({
|
||||||
action: "lists.create",
|
action: "lists.create",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
return normalizeList(data);
|
return normalizeList(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateList(id: string, payload: UpdateListPayload): Promise<boolean> {
|
export function updateList(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateListPayload,
|
||||||
|
): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "lists.update",
|
action: "lists.update",
|
||||||
id,
|
id,
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteList(id: string): Promise<boolean> {
|
export function deleteList(id: string): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "lists.delete",
|
action: "lists.delete",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
type UnknownObject = Record<string, unknown>;
|
type UnknownObject = Record<string, unknown>;
|
||||||
|
|
||||||
function asObject(value: unknown): UnknownObject | undefined {
|
function asObject(value: unknown): UnknownObject | undefined {
|
||||||
return value && typeof value === "object" ? (value as UnknownObject) : undefined;
|
return value && typeof value === "object"
|
||||||
|
? (value as UnknownObject)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickCase<T>(
|
export function pickCase<T>(
|
||||||
source: unknown,
|
source: unknown,
|
||||||
camelKey: string,
|
camelKey: string,
|
||||||
pascalKey: string,
|
pascalKey: string,
|
||||||
fallback: T
|
fallback: T,
|
||||||
): T {
|
): T {
|
||||||
const obj = asObject(source);
|
const obj = asObject(source);
|
||||||
if (!obj) return fallback;
|
if (!obj) return fallback;
|
||||||
|
|||||||
@ -37,32 +37,40 @@ type EntryTemplateSaveResultDtoRaw = {
|
|||||||
FilePath?: string;
|
FilePath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeTemplateItem(raw: EntryTemplateItemDtoRaw): EntryTemplateItemDto {
|
function normalizeTemplateItem(
|
||||||
|
raw: EntryTemplateItemDtoRaw,
|
||||||
|
): EntryTemplateItemDto {
|
||||||
return {
|
return {
|
||||||
fileName: pickCase(raw, "fileName", "FileName", ""),
|
fileName: pickCase(raw, "fileName", "FileName", ""),
|
||||||
filePath: pickCase(raw, "filePath", "FilePath", "")
|
filePath: pickCase(raw, "filePath", "FilePath", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listEntryTemplates(dataDirectory?: string): Promise<EntryTemplateItemDto[]> {
|
export async function listEntryTemplates(
|
||||||
|
dataDirectory?: string,
|
||||||
|
): Promise<EntryTemplateItemDto[]> {
|
||||||
const data = await sendCommand<EntryTemplateItemDtoRaw[]>({
|
const data = await sendCommand<EntryTemplateItemDtoRaw[]>({
|
||||||
action: "templates.list",
|
action: "templates.list",
|
||||||
payload: { dataDirectory }
|
payload: { dataDirectory },
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.map(normalizeTemplateItem).filter((item) => Boolean(item.filePath));
|
return data
|
||||||
|
.map(normalizeTemplateItem)
|
||||||
|
.filter((item) => Boolean(item.filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadEntryTemplate(filePath: string): Promise<EntryTemplateLoadResultDto> {
|
export async function loadEntryTemplate(
|
||||||
|
filePath: string,
|
||||||
|
): Promise<EntryTemplateLoadResultDto> {
|
||||||
const data = await sendCommand<EntryTemplateLoadResultDtoRaw>({
|
const data = await sendCommand<EntryTemplateLoadResultDtoRaw>({
|
||||||
action: "templates.load",
|
action: "templates.load",
|
||||||
payload: { filePath }
|
payload: { filePath },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileName: pickCase(data, "fileName", "FileName", ""),
|
fileName: pickCase(data, "fileName", "FileName", ""),
|
||||||
filePath: pickCase(data, "filePath", "FilePath", ""),
|
filePath: pickCase(data, "filePath", "FilePath", ""),
|
||||||
content: pickCase(data, "content", "Content", "")
|
content: pickCase(data, "content", "Content", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,17 +82,17 @@ export async function saveEntryTemplate(payload: {
|
|||||||
}): Promise<EntryTemplateSaveResultDto> {
|
}): Promise<EntryTemplateSaveResultDto> {
|
||||||
const data = await sendCommand<EntryTemplateSaveResultDtoRaw>({
|
const data = await sendCommand<EntryTemplateSaveResultDtoRaw>({
|
||||||
action: "templates.save",
|
action: "templates.save",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePath: pickCase(data, "filePath", "FilePath", "")
|
filePath: pickCase(data, "filePath", "FilePath", ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEntryTemplate(filePath: string): Promise<boolean> {
|
export async function deleteEntryTemplate(filePath: string): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "templates.delete",
|
action: "templates.delete",
|
||||||
payload: { filePath }
|
payload: { filePath },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,7 @@ function normalizeItem(raw: TodoItemDtoRaw): TodoItemDto {
|
|||||||
listId: pickCase(raw, "listId", "ListId", ""),
|
listId: pickCase(raw, "listId", "ListId", ""),
|
||||||
text: pickCase(raw, "text", "Text", ""),
|
text: pickCase(raw, "text", "Text", ""),
|
||||||
done: pickCase(raw, "done", "Done", false),
|
done: pickCase(raw, "done", "Done", false),
|
||||||
sortOrder: pickCase(raw, "sortOrder", "SortOrder", 0)
|
sortOrder: pickCase(raw, "sortOrder", "SortOrder", 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,13 +76,13 @@ function normalizeList(raw: TodoListDtoRaw): TodoListDto {
|
|||||||
id: pickCase(raw, "id", "Id", ""),
|
id: pickCase(raw, "id", "Id", ""),
|
||||||
label: pickCase(raw, "label", "Label", ""),
|
label: pickCase(raw, "label", "Label", ""),
|
||||||
createdAt: pickCase(raw, "createdAt", "CreatedAt", ""),
|
createdAt: pickCase(raw, "createdAt", "CreatedAt", ""),
|
||||||
items: rawItems.map(normalizeItem)
|
items: rawItems.map(normalizeItem),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listTodoLists(): Promise<TodoListDto[]> {
|
export async function listTodoLists(): Promise<TodoListDto[]> {
|
||||||
const data = await sendCommand<TodoListDtoRaw[]>({
|
const data = await sendCommand<TodoListDtoRaw[]>({
|
||||||
action: "todos.list"
|
action: "todos.list",
|
||||||
});
|
});
|
||||||
return data.map(normalizeList).filter((item) => Boolean(item.id));
|
return data.map(normalizeList).filter((item) => Boolean(item.id));
|
||||||
}
|
}
|
||||||
@ -90,55 +90,65 @@ export async function listTodoLists(): Promise<TodoListDto[]> {
|
|||||||
export async function getTodoList(id: string): Promise<TodoListDto | null> {
|
export async function getTodoList(id: string): Promise<TodoListDto | null> {
|
||||||
const data = await sendCommand<TodoListDtoRaw | null>({
|
const data = await sendCommand<TodoListDtoRaw | null>({
|
||||||
action: "todos.get",
|
action: "todos.get",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const normalized = normalizeList(data);
|
const normalized = normalizeList(data);
|
||||||
return normalized.id ? normalized : null;
|
return normalized.id ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTodoList(payload: CreateTodoListPayload): Promise<TodoListDto> {
|
export async function createTodoList(
|
||||||
|
payload: CreateTodoListPayload,
|
||||||
|
): Promise<TodoListDto> {
|
||||||
const data = await sendCommand<TodoListDtoRaw>({
|
const data = await sendCommand<TodoListDtoRaw>({
|
||||||
action: "todos.create",
|
action: "todos.create",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
return normalizeList(data);
|
return normalizeList(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTodoList(id: string, payload: UpdateTodoListPayload): Promise<boolean> {
|
export function updateTodoList(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateTodoListPayload,
|
||||||
|
): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "todos.update",
|
action: "todos.update",
|
||||||
id,
|
id,
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTodoList(id: string): Promise<boolean> {
|
export function deleteTodoList(id: string): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "todos.delete",
|
action: "todos.delete",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTodoItem(payload: CreateTodoItemPayload): Promise<TodoItemDto> {
|
export async function createTodoItem(
|
||||||
|
payload: CreateTodoItemPayload,
|
||||||
|
): Promise<TodoItemDto> {
|
||||||
const data = await sendCommand<TodoItemDtoRaw>({
|
const data = await sendCommand<TodoItemDtoRaw>({
|
||||||
action: "todos.items.create",
|
action: "todos.items.create",
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
return normalizeItem(data);
|
return normalizeItem(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTodoItem(id: string, payload: UpdateTodoItemPayload): Promise<boolean> {
|
export function updateTodoItem(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateTodoItemPayload,
|
||||||
|
): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "todos.items.update",
|
action: "todos.items.update",
|
||||||
id,
|
id,
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTodoItem(id: string): Promise<boolean> {
|
export function deleteTodoItem(id: string): Promise<boolean> {
|
||||||
return sendCommand<boolean>({
|
return sendCommand<boolean>({
|
||||||
action: "todos.items.delete",
|
action: "todos.items.delete",
|
||||||
id
|
id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,3 @@ export type BackendCommand = {
|
|||||||
export type BackendOk<T> = { ok: true; data: T };
|
export type BackendOk<T> = { ok: true; data: T };
|
||||||
export type BackendErr = { ok: false; error: string };
|
export type BackendErr = { ok: false; error: string };
|
||||||
export type BackendResponse<T> = BackendOk<T> | BackendErr;
|
export type BackendResponse<T> = BackendOk<T> | BackendErr;
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
@ -67,9 +68,15 @@
|
|||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
{#if showCancel}
|
{#if showCancel}
|
||||||
<button type="button" class="secondary" on:click={handleCancel}>{cancelText}</button>
|
<button type="button" class="secondary" on:click={handleCancel}
|
||||||
|
>{cancelText}</button
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<button type="button" class:danger={tone === "danger"} on:click={handleConfirm}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class:danger={tone === "danger"}
|
||||||
|
on:click={handleConfirm}
|
||||||
|
>
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,31 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let onVisibleMonthChange: (month: { year: number; month: number; label: string }) => void = () => {};
|
export let onVisibleMonthChange: (month: {
|
||||||
export let onSelectedDateChange: (payload: { year: number; month: number; day: number; key: string }) => void =
|
year: number;
|
||||||
() => {};
|
month: number;
|
||||||
export let onDateActivate: (payload: { year: number; month: number; day: number; key: string }) => void = () => {};
|
label: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
export let onSelectedDateChange: (payload: {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
key: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
export let onDateActivate: (payload: {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
key: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
let currentYear = today.getFullYear();
|
let currentYear = today.getFullYear();
|
||||||
let currentMonth = today.getMonth();
|
let currentMonth = today.getMonth();
|
||||||
let selectedDateKey = getDateKey(today.getFullYear(), today.getMonth(), today.getDate());
|
let selectedDateKey = getDateKey(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate(),
|
||||||
|
);
|
||||||
|
|
||||||
const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||||
|
|
||||||
@ -46,7 +64,7 @@
|
|||||||
year: cell.year,
|
year: cell.year,
|
||||||
month: cell.month,
|
month: cell.month,
|
||||||
day: cell.day,
|
day: cell.day,
|
||||||
key
|
key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,14 +80,20 @@
|
|||||||
for (let i = 0; i < startOffset; i += 1) {
|
for (let i = 0; i < startOffset; i += 1) {
|
||||||
const day = prevMonthLastDay - startOffset + i + 1;
|
const day = prevMonthLastDay - startOffset + i + 1;
|
||||||
const prevMonthDate = new Date(year, month - 1, day);
|
const prevMonthDate = new Date(year, month - 1, day);
|
||||||
const key = getDateKey(prevMonthDate.getFullYear(), prevMonthDate.getMonth(), day);
|
const key = getDateKey(
|
||||||
|
prevMonthDate.getFullYear(),
|
||||||
|
prevMonthDate.getMonth(),
|
||||||
|
day,
|
||||||
|
);
|
||||||
nextCells.push({
|
nextCells.push({
|
||||||
day,
|
day,
|
||||||
month: prevMonthDate.getMonth(),
|
month: prevMonthDate.getMonth(),
|
||||||
year: prevMonthDate.getFullYear(),
|
year: prevMonthDate.getFullYear(),
|
||||||
inMonth: false,
|
inMonth: false,
|
||||||
isToday: key === getDateKey(today.getFullYear(), today.getMonth(), today.getDate()),
|
isToday:
|
||||||
isSelected: key === selectedDateKey
|
key ===
|
||||||
|
getDateKey(today.getFullYear(), today.getMonth(), today.getDate()),
|
||||||
|
isSelected: key === selectedDateKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,43 +104,73 @@
|
|||||||
month,
|
month,
|
||||||
year,
|
year,
|
||||||
inMonth: true,
|
inMonth: true,
|
||||||
isToday: key === getDateKey(today.getFullYear(), today.getMonth(), today.getDate()),
|
isToday:
|
||||||
isSelected: key === selectedDateKey
|
key ===
|
||||||
|
getDateKey(today.getFullYear(), today.getMonth(), today.getDate()),
|
||||||
|
isSelected: key === selectedDateKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const trailing = (7 - (nextCells.length % 7)) % 7;
|
const trailing = (7 - (nextCells.length % 7)) % 7;
|
||||||
for (let day = 1; day <= trailing; day += 1) {
|
for (let day = 1; day <= trailing; day += 1) {
|
||||||
const nextMonthDate = new Date(year, month + 1, day);
|
const nextMonthDate = new Date(year, month + 1, day);
|
||||||
const key = getDateKey(nextMonthDate.getFullYear(), nextMonthDate.getMonth(), day);
|
const key = getDateKey(
|
||||||
|
nextMonthDate.getFullYear(),
|
||||||
|
nextMonthDate.getMonth(),
|
||||||
|
day,
|
||||||
|
);
|
||||||
nextCells.push({
|
nextCells.push({
|
||||||
day,
|
day,
|
||||||
month: nextMonthDate.getMonth(),
|
month: nextMonthDate.getMonth(),
|
||||||
year: nextMonthDate.getFullYear(),
|
year: nextMonthDate.getFullYear(),
|
||||||
inMonth: false,
|
inMonth: false,
|
||||||
isToday: key === getDateKey(today.getFullYear(), today.getMonth(), today.getDate()),
|
isToday:
|
||||||
isSelected: key === selectedDateKey
|
key ===
|
||||||
|
getDateKey(today.getFullYear(), today.getMonth(), today.getDate()),
|
||||||
|
isSelected: key === selectedDateKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextCells;
|
return nextCells;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: monthLabel = new Date(currentYear, currentMonth, 1).toLocaleString(undefined, { month: "long" });
|
$: monthLabel = new Date(currentYear, currentMonth, 1).toLocaleString(
|
||||||
|
undefined,
|
||||||
|
{ month: "long" },
|
||||||
|
);
|
||||||
$: cells = getCalendarCells(currentYear, currentMonth);
|
$: cells = getCalendarCells(currentYear, currentMonth);
|
||||||
$: onVisibleMonthChange({ year: currentYear, month: currentMonth, label: monthLabel });
|
$: onVisibleMonthChange({
|
||||||
|
year: currentYear,
|
||||||
|
month: currentMonth,
|
||||||
|
label: monthLabel,
|
||||||
|
});
|
||||||
$: {
|
$: {
|
||||||
const parts = selectedDateKey.split("-");
|
const parts = selectedDateKey.split("-");
|
||||||
const [year, month, day] = parts.map((value) => Number(value));
|
const [year, month, day] = parts.map((value) => Number(value));
|
||||||
if (parts.length === 3 && !Number.isNaN(year) && !Number.isNaN(month) && !Number.isNaN(day)) {
|
if (
|
||||||
onSelectedDateChange({ year, month: month - 1, day, key: selectedDateKey });
|
parts.length === 3 &&
|
||||||
|
!Number.isNaN(year) &&
|
||||||
|
!Number.isNaN(month) &&
|
||||||
|
!Number.isNaN(day)
|
||||||
|
) {
|
||||||
|
onSelectedDateChange({
|
||||||
|
year,
|
||||||
|
month: month - 1,
|
||||||
|
day,
|
||||||
|
key: selectedDateKey,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="calendar-widget" aria-label="Monthly calendar">
|
<section class="calendar-widget" aria-label="Monthly calendar">
|
||||||
<header class="calendar-header">
|
<header class="calendar-header">
|
||||||
<button type="button" class="nav-icon" aria-label="Previous month" on:click={() => changeMonth(-1)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="nav-icon"
|
||||||
|
aria-label="Previous month"
|
||||||
|
on:click={() => changeMonth(-1)}
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">chevron_left</span>
|
<span class="material-symbols-outlined">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -125,7 +179,12 @@
|
|||||||
<span>{currentYear}</span>
|
<span>{currentYear}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="nav-icon" aria-label="Next month" on:click={() => changeMonth(1)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="nav-icon"
|
||||||
|
aria-label="Next month"
|
||||||
|
on:click={() => changeMonth(1)}
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">chevron_right</span>
|
<span class="material-symbols-outlined">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FragmentEditor from "$lib/components/editor/FragmentEditor.svelte";
|
import FragmentEditor from "$lib/components/editor/FragmentEditor.svelte";
|
||||||
import ListEditor from "$lib/components/editor/ListEditor.svelte";
|
import ListEditor from "$lib/components/editor/ListEditor.svelte";
|
||||||
@ -23,7 +24,11 @@
|
|||||||
export let onDeleteDocument: (id: string) => void = () => {};
|
export let onDeleteDocument: (id: string) => void = () => {};
|
||||||
export let showLinkedBackButton = false;
|
export let showLinkedBackButton = false;
|
||||||
export let onLinkedBack: () => void = () => {};
|
export let onLinkedBack: () => void = () => {};
|
||||||
export let calendarItems: Array<{ id: string; label: string; initialContent: string }> = [];
|
export let calendarItems: Array<{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
initialContent: string;
|
||||||
|
}> = [];
|
||||||
export let calendarBusy = false;
|
export let calendarBusy = false;
|
||||||
export let calendarError = "";
|
export let calendarError = "";
|
||||||
export let previewOnly = true;
|
export let previewOnly = true;
|
||||||
@ -69,16 +74,25 @@
|
|||||||
return label?.trim() || "Untitled Entry";
|
return label?.trim() || "Untitled Entry";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCalendarCard(item: { id: string; label: string; initialContent: string }): CalendarCard {
|
function toCalendarCard(item: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
initialContent: string;
|
||||||
|
}): CalendarCard {
|
||||||
const content = item.initialContent ?? "";
|
const content = item.initialContent ?? "";
|
||||||
const lower = content.toLowerCase();
|
const lower = content.toLowerCase();
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
title: deriveTitle(item.label, content),
|
title: deriveTitle(item.label, content),
|
||||||
summary: deriveSummary(content),
|
summary: deriveSummary(content),
|
||||||
hasTrigger: lower.includes("!trigger") || lower.includes("#trigger") || lower.includes("#stress"),
|
hasTrigger:
|
||||||
hasMood: lower.includes("mental / emotional snapshot") || lower.includes("cognitive state"),
|
lower.includes("!trigger") ||
|
||||||
hasOpenTodos: /-\s*\[\s\]/.test(content)
|
lower.includes("#trigger") ||
|
||||||
|
lower.includes("#stress"),
|
||||||
|
hasMood:
|
||||||
|
lower.includes("mental / emotional snapshot") ||
|
||||||
|
lower.includes("cognitive state"),
|
||||||
|
hasOpenTodos: /-\s*\[\s\]/.test(content),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,8 +102,15 @@
|
|||||||
<main class="editor-panel" aria-label="Editor area">
|
<main class="editor-panel" aria-label="Editor area">
|
||||||
{#if showLinkedBackButton}
|
{#if showLinkedBackButton}
|
||||||
<div class="editor-nav">
|
<div class="editor-nav">
|
||||||
<button type="button" class="back-btn" on:click={onLinkedBack} aria-label="Back to source entry">
|
<button
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">arrow_back</span>
|
type="button"
|
||||||
|
class="back-btn"
|
||||||
|
on:click={onLinkedBack}
|
||||||
|
aria-label="Back to source entry"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>arrow_back</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -109,7 +130,11 @@
|
|||||||
<ul class="calendar-list">
|
<ul class="calendar-list">
|
||||||
{#each calendarCards as item}
|
{#each calendarCards as item}
|
||||||
<li class:is-active={item.id === openDocumentId}>
|
<li class:is-active={item.id === openDocumentId}>
|
||||||
<button type="button" class="calendar-item-btn" on:click={() => onOpenDocument(item)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="calendar-item-btn"
|
||||||
|
on:click={() => onOpenDocument(item)}
|
||||||
|
>
|
||||||
<div class="calendar-item-head">
|
<div class="calendar-item-head">
|
||||||
<h3>{item.title}</h3>
|
<h3>{item.title}</h3>
|
||||||
<span class="calendar-date">{item.label}</span>
|
<span class="calendar-date">{item.label}</span>
|
||||||
@ -117,8 +142,11 @@
|
|||||||
<p class="calendar-summary">{item.summary}</p>
|
<p class="calendar-summary">{item.summary}</p>
|
||||||
<div class="calendar-badges">
|
<div class="calendar-badges">
|
||||||
{#if item.hasMood}<span class="badge mood">Mood</span>{/if}
|
{#if item.hasMood}<span class="badge mood">Mood</span>{/if}
|
||||||
{#if item.hasTrigger}<span class="badge trigger">Trigger</span>{/if}
|
{#if item.hasTrigger}<span class="badge trigger">Trigger</span
|
||||||
{#if item.hasOpenTodos}<span class="badge todo">Open To-Dos</span>{/if}
|
>{/if}
|
||||||
|
{#if item.hasOpenTodos}<span class="badge todo"
|
||||||
|
>Open To-Dos</span
|
||||||
|
>{/if}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let activeSection: string | null = "entries";
|
export let activeSection: string | null = "entries";
|
||||||
export let onSelect: (id: string) => void = () => {};
|
export let onSelect: (id: string) => void = () => {};
|
||||||
@ -13,7 +14,7 @@
|
|||||||
{ id: "calendar", label: "Calendar", icon: "calendar_month" },
|
{ id: "calendar", label: "Calendar", icon: "calendar_month" },
|
||||||
{ id: "fragments", label: "Fragments", icon: "auto_stories" },
|
{ id: "fragments", label: "Fragments", icon: "auto_stories" },
|
||||||
{ id: "todos", label: "To-Do List", icon: "checklist" },
|
{ id: "todos", label: "To-Do List", icon: "checklist" },
|
||||||
{ id: "lists", label: "Lists", icon: "lists" }
|
{ id: "lists", label: "Lists", icon: "lists" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function selectItem(id: string) {
|
function selectItem(id: string) {
|
||||||
@ -64,7 +65,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 14px 10px;
|
padding: 14px 10px;
|
||||||
background: linear-gradient(180deg, var(--surface-2) 0%, var(--bg-navbar) 100%);
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--surface-2) 0%,
|
||||||
|
var(--bg-navbar) 100%
|
||||||
|
);
|
||||||
border-right: 1px solid var(--border-soft);
|
border-right: 1px solid var(--border-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +107,10 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.14s ease, color 0.14s ease, border-color 0.14s ease;
|
transition:
|
||||||
|
background-color 0.14s ease,
|
||||||
|
color 0.14s ease,
|
||||||
|
border-color 0.14s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button .material-symbols-outlined {
|
.nav-button .material-symbols-outlined {
|
||||||
@ -129,7 +137,9 @@
|
|||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||||
transition: opacity 0.12s ease, transform 0.12s ease;
|
transition:
|
||||||
|
opacity 0.12s ease,
|
||||||
|
transform 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button:hover,
|
.nav-button:hover,
|
||||||
|
|||||||
@ -1,23 +1,60 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { listEntryTemplates, type EntryTemplateItemDto } from "$lib/backend/templates";
|
import {
|
||||||
|
listEntryTemplates,
|
||||||
|
type EntryTemplateItemDto,
|
||||||
|
} from "$lib/backend/templates";
|
||||||
import { listFragments, type FragmentDto } from "$lib/backend/fragments";
|
import { listFragments, type FragmentDto } from "$lib/backend/fragments";
|
||||||
import { listLists, type ListDocumentDto } from "$lib/backend/lists";
|
import { listLists, type ListDocumentDto } from "$lib/backend/lists";
|
||||||
import { listTodoLists, type TodoListDto } from "$lib/backend/todos";
|
import { listTodoLists, type TodoListDto } from "$lib/backend/todos";
|
||||||
import { sendCommand } from "$lib/backend/client";
|
import { sendCommand } from "$lib/backend/client";
|
||||||
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
||||||
import { entriesBusyStore, entriesStore, searchEntriesAsItems } from "$lib/stores/entries";
|
import {
|
||||||
import { createFragmentDraft, fragmentsStore, serializeFragment } from "$lib/stores/fragments";
|
entriesBusyStore,
|
||||||
import { createListDraft, createListFromLabel, listsStore } from "$lib/stores/lists";
|
entriesStore,
|
||||||
import { createTodoListDraft, createTodoListFromLabel, serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
searchEntriesAsItems,
|
||||||
|
} from "$lib/stores/entries";
|
||||||
|
import {
|
||||||
|
createFragmentDraft,
|
||||||
|
fragmentsStore,
|
||||||
|
serializeFragment,
|
||||||
|
} from "$lib/stores/fragments";
|
||||||
|
import {
|
||||||
|
createListDraft,
|
||||||
|
createListFromLabel,
|
||||||
|
listsStore,
|
||||||
|
} from "$lib/stores/lists";
|
||||||
|
import {
|
||||||
|
createTodoListDraft,
|
||||||
|
createTodoListFromLabel,
|
||||||
|
serializeTodoList,
|
||||||
|
todoListsStore,
|
||||||
|
todosStore,
|
||||||
|
} from "$lib/stores/todos";
|
||||||
import { extractEntryTags } from "$lib/utils/metadata";
|
import { extractEntryTags } from "$lib/utils/metadata";
|
||||||
|
|
||||||
export let activeSection = "entries";
|
export let activeSection = "entries";
|
||||||
export let activeDocumentId = "";
|
export let activeDocumentId = "";
|
||||||
export let templateRefreshToken = 0;
|
export let templateRefreshToken = 0;
|
||||||
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
export let onOpenDocument: (doc: {
|
||||||
export let onEditItem: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
id: string;
|
||||||
export let onDeleteItem: (doc: { id: string; label: string }) => void = () => {};
|
label: string;
|
||||||
export let onCalendarStateChange: (state: { items: SidePanelItem[]; busy: boolean; error: string }) => void = () => {};
|
initialContent: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
export let onEditItem: (doc: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
initialContent: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
export let onDeleteItem: (doc: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
export let onCalendarStateChange: (state: {
|
||||||
|
items: SidePanelItem[];
|
||||||
|
busy: boolean;
|
||||||
|
error: string;
|
||||||
|
}) => void = () => {};
|
||||||
|
|
||||||
let showNewItemInput = false;
|
let showNewItemInput = false;
|
||||||
let newItemName = "";
|
let newItemName = "";
|
||||||
@ -56,14 +93,18 @@
|
|||||||
calendar: "Calendar",
|
calendar: "Calendar",
|
||||||
fragments: "Fragments",
|
fragments: "Fragments",
|
||||||
todos: "To-Do List",
|
todos: "To-Do List",
|
||||||
lists: "Lists"
|
lists: "Lists",
|
||||||
};
|
};
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
let calendarYear = today.getFullYear();
|
let calendarYear = today.getFullYear();
|
||||||
let calendarMonth = today.getMonth();
|
let calendarMonth = today.getMonth();
|
||||||
let calendarMonthLabel = new Date(calendarYear, calendarMonth, 1).toLocaleString(undefined, {
|
let calendarMonthLabel = new Date(
|
||||||
month: "long"
|
calendarYear,
|
||||||
|
calendarMonth,
|
||||||
|
1,
|
||||||
|
).toLocaleString(undefined, {
|
||||||
|
month: "long",
|
||||||
});
|
});
|
||||||
let calendarViewMode: CalendarViewMode = "month";
|
let calendarViewMode: CalendarViewMode = "month";
|
||||||
let calendarSortMode: CalendarSortMode = "desc";
|
let calendarSortMode: CalendarSortMode = "desc";
|
||||||
@ -86,14 +127,23 @@
|
|||||||
let calendarDateExplicitlySelected = false;
|
let calendarDateExplicitlySelected = false;
|
||||||
const CALENDAR_SAVED_VIEWS_KEY = "journal.calendar.savedViews";
|
const CALENDAR_SAVED_VIEWS_KEY = "journal.calendar.savedViews";
|
||||||
|
|
||||||
let selectedCalendarDate: { year: number; month: number; day: number; key: string } | null = {
|
let selectedCalendarDate: {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
key: string;
|
||||||
|
} | null = {
|
||||||
year: today.getFullYear(),
|
year: today.getFullYear(),
|
||||||
month: today.getMonth(),
|
month: today.getMonth(),
|
||||||
day: today.getDate(),
|
day: today.getDate(),
|
||||||
key: `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`
|
key: `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleVisibleMonthChange(payload: { year: number; month: number; label: string }) {
|
function handleVisibleMonthChange(payload: {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
calendarYear = payload.year;
|
calendarYear = payload.year;
|
||||||
calendarMonth = payload.month;
|
calendarMonth = payload.month;
|
||||||
calendarMonthLabel = payload.label;
|
calendarMonthLabel = payload.label;
|
||||||
@ -111,7 +161,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseDateLabel(value: string): Date | null {
|
function parseDateLabel(value: string): Date | null {
|
||||||
const token = value.trim().split(/\s*[|·]\s*/)[0].trim();
|
const token = value
|
||||||
|
.trim()
|
||||||
|
.split(/\s*[|·]\s*/)[0]
|
||||||
|
.trim();
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(token)) return null;
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(token)) return null;
|
||||||
const date = new Date(`${token}T00:00:00`);
|
const date = new Date(`${token}T00:00:00`);
|
||||||
return Number.isNaN(date.getTime()) ? null : date;
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
@ -127,7 +180,11 @@
|
|||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWithinRange(date: Date, startDate: string, endDate: string): boolean {
|
function isWithinRange(
|
||||||
|
date: Date,
|
||||||
|
startDate: string,
|
||||||
|
endDate: string,
|
||||||
|
): boolean {
|
||||||
const start = parseDateLabel(startDate);
|
const start = parseDateLabel(startDate);
|
||||||
const end = parseDateLabel(endDate);
|
const end = parseDateLabel(endDate);
|
||||||
if (!start || !end) return true;
|
if (!start || !end) return true;
|
||||||
@ -149,7 +206,7 @@
|
|||||||
const selected = selectedCalendarDate ?? {
|
const selected = selectedCalendarDate ?? {
|
||||||
year: calendarYear,
|
year: calendarYear,
|
||||||
month: calendarMonth,
|
month: calendarMonth,
|
||||||
day: 1
|
day: 1,
|
||||||
};
|
};
|
||||||
const selectedDate = new Date(selected.year, selected.month, selected.day);
|
const selectedDate = new Date(selected.year, selected.month, selected.day);
|
||||||
|
|
||||||
@ -173,8 +230,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getDataDirectory(): Promise<string> {
|
async function getDataDirectory(): Promise<string> {
|
||||||
const config = await sendCommand<{ dataDirectory?: string; DataDirectory?: string }>({
|
const config = await sendCommand<{
|
||||||
action: "config.get"
|
dataDirectory?: string;
|
||||||
|
DataDirectory?: string;
|
||||||
|
}>({
|
||||||
|
action: "config.get",
|
||||||
});
|
});
|
||||||
return (config.dataDirectory ?? config.DataDirectory ?? "").trim();
|
return (config.dataDirectory ?? config.DataDirectory ?? "").trim();
|
||||||
}
|
}
|
||||||
@ -182,7 +242,8 @@
|
|||||||
function toFragmentTimelineItem(fragment: FragmentDto): SidePanelItem {
|
function toFragmentTimelineItem(fragment: FragmentDto): SidePanelItem {
|
||||||
const split = fragment.description.split(/\n{2,}/);
|
const split = fragment.description.split(/\n{2,}/);
|
||||||
const title = (split[0] ?? "").trim() || "Untitled Fragment";
|
const title = (split[0] ?? "").trim() || "Untitled Fragment";
|
||||||
const body = split.slice(1).join("\n\n").trim() || "Add details for this fragment.";
|
const body =
|
||||||
|
split.slice(1).join("\n\n").trim() || "Add details for this fragment.";
|
||||||
const date = parseIsoDate(fragment.time) ?? new Date();
|
const date = parseIsoDate(fragment.time) ?? new Date();
|
||||||
const dateKey = toIsoDate(date);
|
const dateKey = toIsoDate(date);
|
||||||
return {
|
return {
|
||||||
@ -192,20 +253,23 @@
|
|||||||
title,
|
title,
|
||||||
type: fragment.type,
|
type: fragment.type,
|
||||||
tags: fragment.tags ?? [],
|
tags: fragment.tags ?? [],
|
||||||
body
|
body,
|
||||||
}),
|
}),
|
||||||
sortDate: date.toISOString()
|
sortDate: date.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toListTimelineItem(list: ListDocumentDto): SidePanelItem {
|
function toListTimelineItem(list: ListDocumentDto): SidePanelItem {
|
||||||
const created = parseIsoDate(list.createdAt) ?? parseIsoDate(list.updatedAt) ?? new Date();
|
const created =
|
||||||
|
parseIsoDate(list.createdAt) ??
|
||||||
|
parseIsoDate(list.updatedAt) ??
|
||||||
|
new Date();
|
||||||
const dateKey = toIsoDate(created);
|
const dateKey = toIsoDate(created);
|
||||||
return {
|
return {
|
||||||
id: `lists/${list.id}`,
|
id: `lists/${list.id}`,
|
||||||
label: `${dateKey} | List | ${list.label}`,
|
label: `${dateKey} | List | ${list.label}`,
|
||||||
initialContent: list.content || `# ${list.label}\n\n`,
|
initialContent: list.content || `# ${list.label}\n\n`,
|
||||||
sortDate: created.toISOString()
|
sortDate: created.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,13 +279,13 @@
|
|||||||
const items = list.items.map((item, index) => ({
|
const items = list.items.map((item, index) => ({
|
||||||
id: Date.now() + index,
|
id: Date.now() + index,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
done: item.done
|
done: item.done,
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
id: `todos/${list.id}`,
|
id: `todos/${list.id}`,
|
||||||
label: `${dateKey} | To-Do | ${list.label}`,
|
label: `${dateKey} | To-Do | ${list.label}`,
|
||||||
initialContent: serializeTodoList(list.label, items),
|
initialContent: serializeTodoList(list.label, items),
|
||||||
sortDate: created.toISOString()
|
sortDate: created.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,9 +299,14 @@
|
|||||||
return tokens.some((token) => lower.includes(token));
|
return tokens.some((token) => lower.includes(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesTags(itemTags: string[] | undefined, tagTokens: string[]): boolean {
|
function matchesTags(
|
||||||
|
itemTags: string[] | undefined,
|
||||||
|
tagTokens: string[],
|
||||||
|
): boolean {
|
||||||
if (tagTokens.length === 0) return true;
|
if (tagTokens.length === 0) return true;
|
||||||
const normalized = (itemTags ?? []).map((tag) => tag.toLowerCase().trim()).filter(Boolean);
|
const normalized = (itemTags ?? [])
|
||||||
|
.map((tag) => tag.toLowerCase().trim())
|
||||||
|
.filter(Boolean);
|
||||||
return tagTokens.some((token) => normalized.includes(token));
|
return tagTokens.some((token) => normalized.includes(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,13 +323,13 @@
|
|||||||
const { startDate, endDate } = getActiveDateRange();
|
const { startDate, endDate } = getActiveDateRange();
|
||||||
const entryItems = await searchEntriesAsItems({
|
const entryItems = await searchEntriesAsItems({
|
||||||
dataDirectory,
|
dataDirectory,
|
||||||
query: calendarQuery.trim() || undefined
|
query: calendarQuery.trim() || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [fragmentDtos, listDtos, todoDtos] = await Promise.all([
|
const [fragmentDtos, listDtos, todoDtos] = await Promise.all([
|
||||||
listFragments(),
|
listFragments(),
|
||||||
listLists(),
|
listLists(),
|
||||||
listTodoLists()
|
listTodoLists(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const query = calendarQuery.trim();
|
const query = calendarQuery.trim();
|
||||||
@ -274,32 +343,40 @@
|
|||||||
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
||||||
if (!matchesTextQuery(fragment.description, query)) return false;
|
if (!matchesTextQuery(fragment.description, query)) return false;
|
||||||
if (!matchesTags(fragment.tags, tagTokens)) return false;
|
if (!matchesTags(fragment.tags, tagTokens)) return false;
|
||||||
if (hasTypeFilter && !matchesAnyToken(fragment.type ?? "", typeTokens)) return false;
|
if (
|
||||||
|
hasTypeFilter &&
|
||||||
|
!matchesAnyToken(fragment.type ?? "", typeTokens)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(toFragmentTimelineItem);
|
.map(toFragmentTimelineItem);
|
||||||
|
|
||||||
const listItems = listDtos
|
const listItems = listDtos.map(toListTimelineItem).filter((item) => {
|
||||||
.map(toListTimelineItem)
|
if (hasTypeFilter) return false;
|
||||||
.filter((item) => {
|
if (tagTokens.length > 0) return false;
|
||||||
if (hasTypeFilter) return false;
|
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
||||||
if (tagTokens.length > 0) return false;
|
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
||||||
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
if (
|
||||||
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
!matchesTextQuery(item.initialContent, query) &&
|
||||||
if (!matchesTextQuery(item.initialContent, query) && !matchesTextQuery(item.label, query)) return false;
|
!matchesTextQuery(item.label, query)
|
||||||
return true;
|
)
|
||||||
});
|
return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const todoItems = todoDtos
|
const todoItems = todoDtos.map(toTodoTimelineItem).filter((item) => {
|
||||||
.map(toTodoTimelineItem)
|
if (hasTypeFilter) return false;
|
||||||
.filter((item) => {
|
if (tagTokens.length > 0) return false;
|
||||||
if (hasTypeFilter) return false;
|
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
||||||
if (tagTokens.length > 0) return false;
|
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
||||||
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
if (
|
||||||
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
!matchesTextQuery(item.initialContent, query) &&
|
||||||
if (!matchesTextQuery(item.initialContent, query) && !matchesTextQuery(item.label, query)) return false;
|
!matchesTextQuery(item.label, query)
|
||||||
return true;
|
)
|
||||||
});
|
return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const entriesWithKind: SidePanelItem[] = entryItems
|
const entriesWithKind: SidePanelItem[] = entryItems
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
@ -309,29 +386,43 @@
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
label: `${dateKey} | Entry | ${item.label}`,
|
label: `${dateKey} | Entry | ${item.label}`,
|
||||||
initialContent: item.initialContent,
|
initialContent: item.initialContent,
|
||||||
sortDate: date ? date.toISOString() : undefined
|
sortDate: date ? date.toISOString() : undefined,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (hasTypeFilter) return false;
|
if (hasTypeFilter) return false;
|
||||||
if (!matchesTags(extractEntryTags(item.initialContent), tagTokens)) return false;
|
if (!matchesTags(extractEntryTags(item.initialContent), tagTokens))
|
||||||
if (!matchesTextQuery(item.initialContent, query) && !matchesTextQuery(item.label, query)) return false;
|
return false;
|
||||||
|
if (
|
||||||
|
!matchesTextQuery(item.initialContent, query) &&
|
||||||
|
!matchesTextQuery(item.label, query)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
||||||
if (date && !isWithinRange(date, startDate, endDate)) return false;
|
if (date && !isWithinRange(date, startDate, endDate)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const merged = [...entriesWithKind, ...fragmentItems, ...listItems, ...todoItems];
|
const merged = [
|
||||||
|
...entriesWithKind,
|
||||||
|
...fragmentItems,
|
||||||
|
...listItems,
|
||||||
|
...todoItems,
|
||||||
|
];
|
||||||
const sorted = merged.sort((a, b) => {
|
const sorted = merged.sort((a, b) => {
|
||||||
const aDate = a.sortDate ? parseIsoDate(a.sortDate)?.getTime() ?? 0 : parseDateLabel(a.label)?.getTime() ?? 0;
|
const aDate = a.sortDate
|
||||||
const bDate = b.sortDate ? parseIsoDate(b.sortDate)?.getTime() ?? 0 : parseDateLabel(b.label)?.getTime() ?? 0;
|
? (parseIsoDate(a.sortDate)?.getTime() ?? 0)
|
||||||
|
: (parseDateLabel(a.label)?.getTime() ?? 0);
|
||||||
|
const bDate = b.sortDate
|
||||||
|
? (parseIsoDate(b.sortDate)?.getTime() ?? 0)
|
||||||
|
: (parseDateLabel(b.label)?.getTime() ?? 0);
|
||||||
return calendarSortMode === "asc" ? aDate - bDate : bDate - aDate;
|
return calendarSortMode === "asc" ? aDate - bDate : bDate - aDate;
|
||||||
});
|
});
|
||||||
calendarTimelineItems = sorted;
|
calendarTimelineItems = sorted;
|
||||||
calendarLastRefreshedAt = new Date().toLocaleTimeString(undefined, {
|
calendarLastRefreshedAt = new Date().toLocaleTimeString(undefined, {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit"
|
second: "2-digit",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
calendarError = String(error);
|
calendarError = String(error);
|
||||||
@ -341,7 +432,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function forceRefreshCalendar(options?: { allowWhileBusy?: boolean }): Promise<void> {
|
async function forceRefreshCalendar(options?: {
|
||||||
|
allowWhileBusy?: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
if (activeSection !== "calendar") return;
|
if (activeSection !== "calendar") return;
|
||||||
if (calendarBusy && !options?.allowWhileBusy) return;
|
if (calendarBusy && !options?.allowWhileBusy) return;
|
||||||
if (calendarTimelineDebounce) {
|
if (calendarTimelineDebounce) {
|
||||||
@ -369,7 +462,10 @@
|
|||||||
|
|
||||||
function persistSavedViews() {
|
function persistSavedViews() {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
window.localStorage.setItem(CALENDAR_SAVED_VIEWS_KEY, JSON.stringify(calendarSavedViews));
|
window.localStorage.setItem(
|
||||||
|
CALENDAR_SAVED_VIEWS_KEY,
|
||||||
|
JSON.stringify(calendarSavedViews),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySavedView(view: SavedCalendarView) {
|
function applySavedView(view: SavedCalendarView) {
|
||||||
@ -399,7 +495,7 @@
|
|||||||
tags: calendarTags,
|
tags: calendarTags,
|
||||||
types: calendarTypes,
|
types: calendarTypes,
|
||||||
startDate: calendarStartDate,
|
startDate: calendarStartDate,
|
||||||
endDate: calendarEndDate
|
endDate: calendarEndDate,
|
||||||
};
|
};
|
||||||
calendarSavedViews = [view, ...calendarSavedViews];
|
calendarSavedViews = [view, ...calendarSavedViews];
|
||||||
persistSavedViews();
|
persistSavedViews();
|
||||||
@ -422,7 +518,7 @@
|
|||||||
tags: "",
|
tags: "",
|
||||||
types: "",
|
types: "",
|
||||||
startDate: "",
|
startDate: "",
|
||||||
endDate: ""
|
endDate: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "builtin-trigger",
|
id: "builtin-trigger",
|
||||||
@ -433,13 +529,16 @@
|
|||||||
tags: "stress, trigger",
|
tags: "stress, trigger",
|
||||||
types: "!TRIGGER",
|
types: "!TRIGGER",
|
||||||
startDate: "",
|
startDate: "",
|
||||||
endDate: ""
|
endDate: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function openOrCreateDailyEntry(dateKey: string) {
|
function openOrCreateDailyEntry(dateKey: string) {
|
||||||
const existing = $entriesStore.find((item) => item.label === dateKey && !item.id.startsWith("entries/template-draft-"));
|
const existing = $entriesStore.find(
|
||||||
|
(item) =>
|
||||||
|
item.label === dateKey &&
|
||||||
|
!item.id.startsWith("entries/template-draft-"),
|
||||||
|
);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
onOpenDocument(existing);
|
onOpenDocument(existing);
|
||||||
return;
|
return;
|
||||||
@ -448,17 +547,27 @@
|
|||||||
const draft: SidePanelItem = {
|
const draft: SidePanelItem = {
|
||||||
id: `entries/draft-${Date.now()}`,
|
id: `entries/draft-${Date.now()}`,
|
||||||
label: dateKey,
|
label: dateKey,
|
||||||
initialContent: `# ${dateKey}\n\n`
|
initialContent: `# ${dateKey}\n\n`,
|
||||||
};
|
};
|
||||||
entriesStore.update((items) => [draft, ...items]);
|
entriesStore.update((items) => [draft, ...items]);
|
||||||
onEditItem(draft);
|
onEditItem(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectedDateChange(payload: { year: number; month: number; day: number; key: string }) {
|
function handleSelectedDateChange(payload: {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
key: string;
|
||||||
|
}) {
|
||||||
selectedCalendarDate = payload;
|
selectedCalendarDate = payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDateActivate(payload: { year: number; month: number; day: number; key: string }) {
|
function handleDateActivate(payload: {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
key: string;
|
||||||
|
}) {
|
||||||
selectedCalendarDate = payload;
|
selectedCalendarDate = payload;
|
||||||
if (activeSection !== "calendar") return;
|
if (activeSection !== "calendar") return;
|
||||||
calendarDateExplicitlySelected = true;
|
calendarDateExplicitlySelected = true;
|
||||||
@ -478,7 +587,7 @@
|
|||||||
return {
|
return {
|
||||||
id: toTemplateStoreId(item.filePath),
|
id: toTemplateStoreId(item.filePath),
|
||||||
label: toTemplateLabel(item.fileName),
|
label: toTemplateLabel(item.fileName),
|
||||||
initialContent: ""
|
initialContent: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,7 +607,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleAddItem() {
|
function handleAddItem() {
|
||||||
if (activeSection === "entries" || activeSection === "todos" || activeSection === "lists") {
|
if (
|
||||||
|
activeSection === "entries" ||
|
||||||
|
activeSection === "todos" ||
|
||||||
|
activeSection === "lists"
|
||||||
|
) {
|
||||||
if (activeSection === "entries") {
|
if (activeSection === "entries") {
|
||||||
createTemplateMode = false;
|
createTemplateMode = false;
|
||||||
}
|
}
|
||||||
@ -519,7 +632,7 @@
|
|||||||
year: calendarYear,
|
year: calendarYear,
|
||||||
month: calendarMonth,
|
month: calendarMonth,
|
||||||
day: 1,
|
day: 1,
|
||||||
key: toDateKey(calendarYear, calendarMonth, 1)
|
key: toDateKey(calendarYear, calendarMonth, 1),
|
||||||
};
|
};
|
||||||
openOrCreateDailyEntry(selected.key);
|
openOrCreateDailyEntry(selected.key);
|
||||||
} else {
|
} else {
|
||||||
@ -553,7 +666,10 @@
|
|||||||
newItemName = "";
|
newItemName = "";
|
||||||
|
|
||||||
const label = toDailyNoteLabel(new Date());
|
const label = toDailyNoteLabel(new Date());
|
||||||
const existing = $entriesStore.find((item) => item.label === label && !item.id.startsWith("entries/template-draft-"));
|
const existing = $entriesStore.find(
|
||||||
|
(item) =>
|
||||||
|
item.label === label && !item.id.startsWith("entries/template-draft-"),
|
||||||
|
);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
onEditItem(existing);
|
onEditItem(existing);
|
||||||
return;
|
return;
|
||||||
@ -581,8 +697,14 @@
|
|||||||
? label
|
? label
|
||||||
: `${label}_template`
|
: `${label}_template`
|
||||||
: label;
|
: label;
|
||||||
const id = isTemplate ? `entries/template-draft-${Date.now()}` : `entries/draft-${Date.now()}`;
|
const id = isTemplate
|
||||||
const item = { id, label: displayLabel, initialContent: `# ${displayLabel}\n\n` };
|
? `entries/template-draft-${Date.now()}`
|
||||||
|
: `entries/draft-${Date.now()}`;
|
||||||
|
const item = {
|
||||||
|
id,
|
||||||
|
label: displayLabel,
|
||||||
|
initialContent: `# ${displayLabel}\n\n`,
|
||||||
|
};
|
||||||
entriesStore.update((items) => [item, ...items]);
|
entriesStore.update((items) => [item, ...items]);
|
||||||
onEditItem(item);
|
onEditItem(item);
|
||||||
createTemplateMode = false;
|
createTemplateMode = false;
|
||||||
@ -597,17 +719,20 @@
|
|||||||
onOpenDocument({
|
onOpenDocument({
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
label: meta.label,
|
label: meta.label,
|
||||||
initialContent: serializeTodoList(meta.label, todoItems)
|
initialContent: serializeTodoList(meta.label, todoItems),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const draft = createTodoListDraft();
|
const draft = createTodoListDraft();
|
||||||
draft.meta.label = label;
|
draft.meta.label = label;
|
||||||
todoListsStore.update((lists) => [draft.meta, ...lists]);
|
todoListsStore.update((lists) => [draft.meta, ...lists]);
|
||||||
todosStore.update((lists) => ({ ...lists, [draft.meta.id]: draft.items }));
|
todosStore.update((lists) => ({
|
||||||
|
...lists,
|
||||||
|
[draft.meta.id]: draft.items,
|
||||||
|
}));
|
||||||
onOpenDocument({
|
onOpenDocument({
|
||||||
id: draft.meta.id,
|
id: draft.meta.id,
|
||||||
label: draft.meta.label,
|
label: draft.meta.label,
|
||||||
initialContent: serializeTodoList(draft.meta.label, draft.items)
|
initialContent: serializeTodoList(draft.meta.label, draft.items),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (activeSection === "lists") {
|
} else if (activeSection === "lists") {
|
||||||
@ -643,28 +768,42 @@
|
|||||||
$: todoDocuments = $todoListsStore.map(({ id, label }) => ({
|
$: todoDocuments = $todoListsStore.map(({ id, label }) => ({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
initialContent: serializeTodoList(label, $todosStore[id] ?? [])
|
initialContent: serializeTodoList(label, $todosStore[id] ?? []),
|
||||||
}));
|
}));
|
||||||
$: items = activeSection === "entries"
|
$: items =
|
||||||
? $entriesStore
|
activeSection === "entries"
|
||||||
: activeSection === "todos"
|
? $entriesStore
|
||||||
? todoDocuments
|
: activeSection === "todos"
|
||||||
: activeSection === "fragments"
|
? todoDocuments
|
||||||
? $fragmentsStore
|
: activeSection === "fragments"
|
||||||
: activeSection === "lists"
|
? $fragmentsStore
|
||||||
? $listsStore
|
: activeSection === "lists"
|
||||||
: [];
|
? $listsStore
|
||||||
|
: [];
|
||||||
$: isCalendarSection = activeSection === "calendar";
|
$: isCalendarSection = activeSection === "calendar";
|
||||||
$: showItemActions = activeSection === "entries" || activeSection === "todos" || activeSection === "lists" || activeSection === "fragments";
|
$: showItemActions =
|
||||||
$: entryItems = activeSection === "entries"
|
activeSection === "entries" ||
|
||||||
? $entriesStore.filter((item) => !item.id.startsWith("entries/template-draft-"))
|
activeSection === "todos" ||
|
||||||
: [];
|
activeSection === "lists" ||
|
||||||
$: templateDraftItems = activeSection === "entries"
|
activeSection === "fragments";
|
||||||
? $entriesStore.filter((item) => item.id.startsWith("entries/template-draft-"))
|
$: entryItems =
|
||||||
: [];
|
activeSection === "entries"
|
||||||
|
? $entriesStore.filter(
|
||||||
|
(item) => !item.id.startsWith("entries/template-draft-"),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
$: templateDraftItems =
|
||||||
|
activeSection === "entries"
|
||||||
|
? $entriesStore.filter((item) =>
|
||||||
|
item.id.startsWith("entries/template-draft-"),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
$: allTemplateItems = [...templateDraftItems, ...templateItems];
|
$: allTemplateItems = [...templateDraftItems, ...templateItems];
|
||||||
|
|
||||||
$: if (activeSection === "entries" && (!wasEntriesSection || templateRefreshToken !== lastTemplateRefreshToken)) {
|
$: if (
|
||||||
|
activeSection === "entries" &&
|
||||||
|
(!wasEntriesSection || templateRefreshToken !== lastTemplateRefreshToken)
|
||||||
|
) {
|
||||||
wasEntriesSection = true;
|
wasEntriesSection = true;
|
||||||
lastTemplateRefreshToken = templateRefreshToken;
|
lastTemplateRefreshToken = templateRefreshToken;
|
||||||
void refreshTemplates();
|
void refreshTemplates();
|
||||||
@ -691,8 +830,12 @@
|
|||||||
calendarTypes,
|
calendarTypes,
|
||||||
calendarStartDate,
|
calendarStartDate,
|
||||||
calendarEndDate,
|
calendarEndDate,
|
||||||
entriesSig: $entriesStore.map((item) => `${item.id}:${item.label}`).join("|"),
|
entriesSig: $entriesStore
|
||||||
fragmentsSig: $fragmentsStore.map((item) => `${item.id}:${item.label}`).join("|"),
|
.map((item) => `${item.id}:${item.label}`)
|
||||||
|
.join("|"),
|
||||||
|
fragmentsSig: $fragmentsStore
|
||||||
|
.map((item) => `${item.id}:${item.label}`)
|
||||||
|
.join("|"),
|
||||||
listsSig: $listsStore.map((item) => `${item.id}:${item.label}`).join("|"),
|
listsSig: $listsStore.map((item) => `${item.id}:${item.label}`).join("|"),
|
||||||
todosSig: $todoListsStore
|
todosSig: $todoListsStore
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
@ -701,9 +844,12 @@
|
|||||||
.join("~");
|
.join("~");
|
||||||
return `${item.id}:${item.label}:${todos}`;
|
return `${item.id}:${item.label}:${todos}`;
|
||||||
})
|
})
|
||||||
.join("|")
|
.join("|"),
|
||||||
});
|
});
|
||||||
$: if (activeSection === "calendar" && calendarTimelineRefreshKey !== lastCalendarTimelineKey) {
|
$: if (
|
||||||
|
activeSection === "calendar" &&
|
||||||
|
calendarTimelineRefreshKey !== lastCalendarTimelineKey
|
||||||
|
) {
|
||||||
lastCalendarTimelineKey = calendarTimelineRefreshKey;
|
lastCalendarTimelineKey = calendarTimelineRefreshKey;
|
||||||
if (calendarTimelineDebounce) {
|
if (calendarTimelineDebounce) {
|
||||||
clearTimeout(calendarTimelineDebounce);
|
clearTimeout(calendarTimelineDebounce);
|
||||||
@ -726,7 +872,7 @@
|
|||||||
onCalendarStateChange({
|
onCalendarStateChange({
|
||||||
items: calendarTimelineItems,
|
items: calendarTimelineItems,
|
||||||
busy: calendarBusy,
|
busy: calendarBusy,
|
||||||
error: calendarError
|
error: calendarError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -736,19 +882,43 @@
|
|||||||
<h2>{panelTitle}</h2>
|
<h2>{panelTitle}</h2>
|
||||||
<div class="panel-header-actions">
|
<div class="panel-header-actions">
|
||||||
{#if activeSection === "calendar"}
|
{#if activeSection === "calendar"}
|
||||||
<button type="button" class="panel-action" aria-label="Refresh calendar" title="Refresh calendar" on:click={handleRefreshClick}>
|
<button
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
type="button"
|
||||||
</button>
|
class="panel-action"
|
||||||
|
aria-label="Refresh calendar"
|
||||||
|
title="Refresh calendar"
|
||||||
|
on:click={handleRefreshClick}
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if activeSection === "entries"}
|
{#if activeSection === "entries"}
|
||||||
<button type="button" class="panel-action" aria-label="Add template" title="Add template" on:click={handleAddTemplate}>
|
<button
|
||||||
<span class="material-symbols-outlined">palette</span>
|
type="button"
|
||||||
</button>
|
class="panel-action"
|
||||||
<button type="button" class="panel-action" aria-label="Add daily note" title="Add daily note" on:click={handleAddDailyNote}>
|
aria-label="Add template"
|
||||||
<span class="material-symbols-outlined">calendar_month</span>
|
title="Add template"
|
||||||
</button>
|
on:click={handleAddTemplate}
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">palette</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="panel-action"
|
||||||
|
aria-label="Add daily note"
|
||||||
|
title="Add daily note"
|
||||||
|
on:click={handleAddDailyNote}
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">calendar_month</span>
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button type="button" class="panel-action" aria-label="Add item" title="Add item" on:click={handleAddItem}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="panel-action"
|
||||||
|
aria-label="Add item"
|
||||||
|
title="Add item"
|
||||||
|
on:click={handleAddItem}
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">add</span>
|
<span class="material-symbols-outlined">add</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -799,12 +969,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendar-control-row">
|
<div class="calendar-control-row">
|
||||||
<input type="text" bind:value={calendarQuery} placeholder="Text query" />
|
<input
|
||||||
<input type="text" bind:value={calendarTags} placeholder="Tags (comma)" />
|
type="text"
|
||||||
|
bind:value={calendarQuery}
|
||||||
|
placeholder="Text query"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={calendarTags}
|
||||||
|
placeholder="Tags (comma)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendar-control-row">
|
<div class="calendar-control-row">
|
||||||
<input type="text" bind:value={calendarTypes} placeholder="Fragment types (comma)" />
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={calendarTypes}
|
||||||
|
placeholder="Fragment types (comma)"
|
||||||
|
/>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -812,9 +994,17 @@
|
|||||||
<h3>Saved Views</h3>
|
<h3>Saved Views</h3>
|
||||||
<div class="saved-view-actions">
|
<div class="saved-view-actions">
|
||||||
{#each builtInViews as view}
|
{#each builtInViews as view}
|
||||||
<button type="button" class="saved-view-btn" on:click={() => applySavedView(view)}>{view.name}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="saved-view-btn"
|
||||||
|
on:click={() => applySavedView(view)}>{view.name}</button
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
<button type="button" class="saved-view-btn" on:click={() => (showSaveViewInput = true)}>Save Current</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="saved-view-btn"
|
||||||
|
on:click={() => (showSaveViewInput = true)}>Save Current</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if showSaveViewInput}
|
{#if showSaveViewInput}
|
||||||
<div class="saved-view-input">
|
<div class="saved-view-input">
|
||||||
@ -837,8 +1027,17 @@
|
|||||||
<ul class="saved-view-list">
|
<ul class="saved-view-list">
|
||||||
{#each calendarSavedViews as view}
|
{#each calendarSavedViews as view}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" class="saved-view-btn" on:click={() => applySavedView(view)}>{view.name}</button>
|
<button
|
||||||
<button type="button" class="saved-view-delete" on:click={() => deleteSavedView(view.id)} aria-label="Delete saved view">
|
type="button"
|
||||||
|
class="saved-view-btn"
|
||||||
|
on:click={() => applySavedView(view)}>{view.name}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="saved-view-delete"
|
||||||
|
on:click={() => deleteSavedView(view.id)}
|
||||||
|
aria-label="Delete saved view"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">delete</span>
|
<span class="material-symbols-outlined">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -856,12 +1055,17 @@
|
|||||||
|
|
||||||
<div class="calendar-entries">
|
<div class="calendar-entries">
|
||||||
<h3>{calendarMonthLabel} {calendarYear} Timeline</h3>
|
<h3>{calendarMonthLabel} {calendarYear} Timeline</h3>
|
||||||
<p class="section-copy">Last refreshed: {calendarLastRefreshedAt || "Not yet"}</p>
|
<p class="section-copy">
|
||||||
|
Last refreshed: {calendarLastRefreshedAt || "Not yet"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="panel-search">
|
<div class="panel-search">
|
||||||
<span class="material-symbols-outlined">search</span>
|
<span class="material-symbols-outlined">search</span>
|
||||||
<input type="text" placeholder={`Search ${panelTitle.toLowerCase()}...`} />
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={`Search ${panelTitle.toLowerCase()}...`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showNewItemInput}
|
{#if showNewItemInput}
|
||||||
@ -900,10 +1104,20 @@
|
|||||||
{item.label}
|
{item.label}
|
||||||
</button>
|
</button>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-action"
|
||||||
|
on:click|stopPropagation={() => onEditItem(item)}
|
||||||
|
aria-label="Edit"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">edit</span>
|
<span class="material-symbols-outlined">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-action item-action-danger"
|
||||||
|
on:click|stopPropagation={() => onDeleteItem(item)}
|
||||||
|
aria-label="Delete"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">delete</span>
|
<span class="material-symbols-outlined">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -932,10 +1146,20 @@
|
|||||||
{item.label}
|
{item.label}
|
||||||
</button>
|
</button>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-action"
|
||||||
|
on:click|stopPropagation={() => onEditItem(item)}
|
||||||
|
aria-label="Edit"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">edit</span>
|
<span class="material-symbols-outlined">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-action item-action-danger"
|
||||||
|
on:click|stopPropagation={() => onDeleteItem(item)}
|
||||||
|
aria-label="Delete"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">delete</span>
|
<span class="material-symbols-outlined">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -963,10 +1187,20 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if showItemActions}
|
{#if showItemActions}
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-action"
|
||||||
|
on:click|stopPropagation={() => onEditItem(item)}
|
||||||
|
aria-label="Edit"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">edit</span>
|
<span class="material-symbols-outlined">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-action item-action-danger"
|
||||||
|
on:click|stopPropagation={() => onDeleteItem(item)}
|
||||||
|
aria-label="Delete"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined">delete</span>
|
<span class="material-symbols-outlined">delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -980,7 +1214,11 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.side-panel {
|
.side-panel {
|
||||||
background: linear-gradient(180deg, var(--surface-2) 0%, var(--bg-panel) 100%);
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--surface-2) 0%,
|
||||||
|
var(--bg-panel) 100%
|
||||||
|
);
|
||||||
border-right: 1px solid var(--border-soft);
|
border-right: 1px solid var(--border-soft);
|
||||||
padding: 16px 14px;
|
padding: 16px 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1312,5 +1550,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.side-panel {
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list .item-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-list .item-action {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.calendar-control-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
createFragmentFromParsed,
|
createFragmentFromParsed,
|
||||||
@ -7,7 +8,7 @@
|
|||||||
parseFragmentContent,
|
parseFragmentContent,
|
||||||
serializeFragment,
|
serializeFragment,
|
||||||
updateFragmentFromParsed,
|
updateFragmentFromParsed,
|
||||||
type FragmentItem
|
type FragmentItem,
|
||||||
} from "$lib/stores/fragments";
|
} from "$lib/stores/fragments";
|
||||||
import { settingsFragmentTypes, settingsTags } from "$lib/stores/settings";
|
import { settingsFragmentTypes, settingsTags } from "$lib/stores/settings";
|
||||||
import { renderMarkdown } from "$lib/utils/markdown";
|
import { renderMarkdown } from "$lib/utils/markdown";
|
||||||
@ -17,7 +18,11 @@
|
|||||||
export let openDocumentName = "";
|
export let openDocumentName = "";
|
||||||
export let openDocumentContent = "";
|
export let openDocumentContent = "";
|
||||||
export let onDocumentContentChange: (content: string) => void = () => {};
|
export let onDocumentContentChange: (content: string) => void = () => {};
|
||||||
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
export let onOpenDocument: (doc: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
initialContent: string;
|
||||||
|
}) => void = () => {};
|
||||||
export let onDeleteDocument: (id: string) => void = () => {};
|
export let onDeleteDocument: (id: string) => void = () => {};
|
||||||
export let externalEditRequested = false;
|
export let externalEditRequested = false;
|
||||||
|
|
||||||
@ -35,18 +40,30 @@
|
|||||||
const customTypeValue = "__custom_type__";
|
const customTypeValue = "__custom_type__";
|
||||||
const customTagValue = "__custom_tag__";
|
const customTagValue = "__custom_tag__";
|
||||||
|
|
||||||
function buildFragmentContent(): { title: string; resolvedType: string; body: string; content: string; tags: string[] } | null {
|
function buildFragmentContent(): {
|
||||||
|
title: string;
|
||||||
|
resolvedType: string;
|
||||||
|
body: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
} | null {
|
||||||
const title = fragmentTitle.trim();
|
const title = fragmentTitle.trim();
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
const resolvedType = fragmentType === customTypeValue ? customFragmentType.trim() : fragmentType;
|
const resolvedType =
|
||||||
|
fragmentType === customTypeValue
|
||||||
|
? customFragmentType.trim()
|
||||||
|
: fragmentType;
|
||||||
if (!resolvedType) return null;
|
if (!resolvedType) return null;
|
||||||
const selectedTags = fragmentTag && fragmentTag !== customTagValue ? [fragmentTag] : [];
|
const selectedTags =
|
||||||
|
fragmentTag && fragmentTag !== customTagValue ? [fragmentTag] : [];
|
||||||
const customTags = customFragmentTags
|
const customTags = customFragmentTags
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((tag) => tag.trim())
|
.map((tag) => tag.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const tagList = [...selectedTags, ...customTags];
|
const tagList = [...selectedTags, ...customTags];
|
||||||
const uniqueTagList = [...new Set(tagList.map((tag) => tag.toLowerCase()))].map((lower) => {
|
const uniqueTagList = [
|
||||||
|
...new Set(tagList.map((tag) => tag.toLowerCase())),
|
||||||
|
].map((lower) => {
|
||||||
return tagList.find((tag) => tag.toLowerCase() === lower) ?? lower;
|
return tagList.find((tag) => tag.toLowerCase() === lower) ?? lower;
|
||||||
});
|
});
|
||||||
const body = fragmentBody.trim() || "Add details for this fragment.";
|
const body = fragmentBody.trim() || "Add details for this fragment.";
|
||||||
@ -54,7 +71,7 @@
|
|||||||
title,
|
title,
|
||||||
type: resolvedType,
|
type: resolvedType,
|
||||||
tags: uniqueTagList,
|
tags: uniqueTagList,
|
||||||
body
|
body,
|
||||||
});
|
});
|
||||||
return { title, resolvedType, body, content, tags: uniqueTagList };
|
return { title, resolvedType, body, content, tags: uniqueTagList };
|
||||||
}
|
}
|
||||||
@ -73,7 +90,7 @@
|
|||||||
title: payload.title,
|
title: payload.title,
|
||||||
type: payload.resolvedType,
|
type: payload.resolvedType,
|
||||||
tags: payload.tags,
|
tags: payload.tags,
|
||||||
body: payload.body
|
body: payload.body,
|
||||||
});
|
});
|
||||||
if (!updated) return;
|
if (!updated) return;
|
||||||
|
|
||||||
@ -92,7 +109,7 @@
|
|||||||
title: payload.title,
|
title: payload.title,
|
||||||
type: payload.resolvedType,
|
type: payload.resolvedType,
|
||||||
tags: payload.tags,
|
tags: payload.tags,
|
||||||
body: payload.body
|
body: payload.body,
|
||||||
});
|
});
|
||||||
onOpenDocument(item);
|
onOpenDocument(item);
|
||||||
fragmentMode = "view";
|
fragmentMode = "view";
|
||||||
@ -141,7 +158,10 @@
|
|||||||
function loadFragmentFormFromDocument() {
|
function loadFragmentFormFromDocument() {
|
||||||
const content = openDocumentContent ?? "";
|
const content = openDocumentContent ?? "";
|
||||||
const isDraftFragment = openDocumentId.startsWith("fragments/new-");
|
const isDraftFragment = openDocumentId.startsWith("fragments/new-");
|
||||||
const parsed = parseFragmentContent(content, openDocumentName || "Untitled Fragment");
|
const parsed = parseFragmentContent(
|
||||||
|
content,
|
||||||
|
openDocumentName || "Untitled Fragment",
|
||||||
|
);
|
||||||
fragmentTitle = parsed.title;
|
fragmentTitle = parsed.title;
|
||||||
const parsedType = parsed.type;
|
const parsedType = parsed.type;
|
||||||
if (!parsedType) {
|
if (!parsedType) {
|
||||||
@ -175,22 +195,35 @@
|
|||||||
fragmentMode = isDraftFragment ? "create" : "view";
|
fragmentMode = isDraftFragment ? "create" : "view";
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fragmentTypeOptions = $settingsFragmentTypes.length ? $settingsFragmentTypes : ["General"];
|
$: fragmentTypeOptions = $settingsFragmentTypes.length
|
||||||
|
? $settingsFragmentTypes
|
||||||
|
: ["General"];
|
||||||
$: tagOptions = $settingsTags;
|
$: tagOptions = $settingsTags;
|
||||||
$: if (openDocumentId !== lastFragmentDocumentId) {
|
$: if (openDocumentId !== lastFragmentDocumentId) {
|
||||||
loadFragmentFormFromDocument();
|
loadFragmentFormFromDocument();
|
||||||
lastFragmentDocumentId = openDocumentId;
|
lastFragmentDocumentId = openDocumentId;
|
||||||
}
|
}
|
||||||
$: if (!fragmentType || (!fragmentTypeOptions.includes(fragmentType) && fragmentType !== customTypeValue)) {
|
$: if (
|
||||||
|
!fragmentType ||
|
||||||
|
(!fragmentTypeOptions.includes(fragmentType) &&
|
||||||
|
fragmentType !== customTypeValue)
|
||||||
|
) {
|
||||||
fragmentType = fragmentTypeOptions[0];
|
fragmentType = fragmentTypeOptions[0];
|
||||||
}
|
}
|
||||||
$: if (!fragmentTag || (!tagOptions.includes(fragmentTag) && fragmentTag !== customTagValue)) {
|
$: if (
|
||||||
|
!fragmentTag ||
|
||||||
|
(!tagOptions.includes(fragmentTag) && fragmentTag !== customTagValue)
|
||||||
|
) {
|
||||||
fragmentTag = tagOptions[0] ?? customTagValue;
|
fragmentTag = tagOptions[0] ?? customTagValue;
|
||||||
}
|
}
|
||||||
$: if (!externalEditRequested) {
|
$: if (!externalEditRequested) {
|
||||||
suppressExternalEditRequest = false;
|
suppressExternalEditRequest = false;
|
||||||
}
|
}
|
||||||
$: if (externalEditRequested && !suppressExternalEditRequest && fragmentMode === "view") {
|
$: if (
|
||||||
|
externalEditRequested &&
|
||||||
|
!suppressExternalEditRequest &&
|
||||||
|
fragmentMode === "view"
|
||||||
|
) {
|
||||||
fragmentMode = "edit";
|
fragmentMode = "edit";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -203,10 +236,17 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<form
|
<form
|
||||||
class="fragment-form"
|
class="fragment-form"
|
||||||
on:submit|preventDefault={fragmentMode === "create" ? createNewFragment : saveFragmentEdits}
|
on:submit|preventDefault={fragmentMode === "create"
|
||||||
|
? createNewFragment
|
||||||
|
: saveFragmentEdits}
|
||||||
>
|
>
|
||||||
<h2>{fragmentMode === "create" ? "New Fragment" : "Edit Fragment"}</h2>
|
<h2>{fragmentMode === "create" ? "New Fragment" : "Edit Fragment"}</h2>
|
||||||
<input type="text" placeholder="Title" bind:value={fragmentTitle} aria-label="Fragment title" />
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Title"
|
||||||
|
bind:value={fragmentTitle}
|
||||||
|
aria-label="Fragment title"
|
||||||
|
/>
|
||||||
<div class="fragment-form-row">
|
<div class="fragment-form-row">
|
||||||
<select bind:value={fragmentType} aria-label="Fragment type">
|
<select bind:value={fragmentType} aria-label="Fragment type">
|
||||||
{#each fragmentTypeOptions as type}
|
{#each fragmentTypeOptions as type}
|
||||||
@ -222,7 +262,12 @@
|
|||||||
aria-label="Custom fragment type"
|
aria-label="Custom fragment type"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<input type="text" value={fragmentType} disabled aria-label="Selected fragment type" />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={fragmentType}
|
||||||
|
disabled
|
||||||
|
aria-label="Selected fragment type"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="fragment-form-row">
|
<div class="fragment-form-row">
|
||||||
@ -252,8 +297,16 @@
|
|||||||
aria-label="Fragment body"
|
aria-label="Fragment body"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="fragment-actions">
|
<div class="fragment-actions">
|
||||||
<button type="submit" class="fragment-submit">{fragmentMode === "create" ? "Create Fragment" : "Save Fragment"}</button>
|
<button type="submit" class="fragment-submit"
|
||||||
<button type="button" class="fragment-secondary" on:click={cancelFragmentEdit}>Cancel</button>
|
>{fragmentMode === "create"
|
||||||
|
? "Create Fragment"
|
||||||
|
: "Save Fragment"}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fragment-secondary"
|
||||||
|
on:click={cancelFragmentEdit}>Cancel</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
@ -292,7 +345,9 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
font-family: "Segoe UI Variable", "Segoe UI", "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", Inter, Roboto, Helvetica, Arial, sans-serif;
|
font-family:
|
||||||
|
"Segoe UI Variable", "Segoe UI", "Segoe UI Emoji", "Apple Color Emoji",
|
||||||
|
"Noto Color Emoji", Inter, Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fragment-view :global(h1),
|
.fragment-view :global(h1),
|
||||||
@ -334,11 +389,17 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: color-mix(in srgb, var(--surface-1) 88%, var(--bg-editor) 12%);
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--surface-1) 88%,
|
||||||
|
var(--bg-editor) 12%
|
||||||
|
);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 10px 11px;
|
padding: 10px 11px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
font-family: "Segoe UI Variable", "Segoe UI", "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", Inter, Roboto, Helvetica, Arial, sans-serif;
|
font-family:
|
||||||
|
"Segoe UI Variable", "Segoe UI", "Segoe UI Emoji", "Apple Color Emoji",
|
||||||
|
"Noto Color Emoji", Inter, Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fragment-form textarea {
|
.fragment-form textarea {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let openDocumentId = "";
|
export let openDocumentId = "";
|
||||||
export let openDocumentName = "";
|
export let openDocumentName = "";
|
||||||
@ -134,14 +135,30 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="list-actions">
|
<div class="list-actions">
|
||||||
<button type="button" class="list-btn save" on:click={saveEditItem}>Save</button>
|
<button
|
||||||
<button type="button" class="list-btn ghost" on:click={cancelEditItem}>Cancel</button>
|
type="button"
|
||||||
|
class="list-btn save"
|
||||||
|
on:click={saveEditItem}>Save</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="list-btn ghost"
|
||||||
|
on:click={cancelEditItem}>Cancel</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="list-text">{item.text}</span>
|
<span class="list-text">{item.text}</span>
|
||||||
<div class="list-actions">
|
<div class="list-actions">
|
||||||
<button type="button" class="list-btn ghost" on:click={() => startEditItem(item.id)}>Edit</button>
|
<button
|
||||||
<button type="button" class="list-btn danger" on:click={() => removeItem(item.id)}>Remove</button>
|
type="button"
|
||||||
|
class="list-btn ghost"
|
||||||
|
on:click={() => startEditItem(item.id)}>Edit</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="list-btn danger"
|
||||||
|
on:click={() => removeItem(item.id)}>Remove</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
@ -266,6 +283,14 @@
|
|||||||
padding: 18px 16px;
|
padding: 18px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-create {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
row-gap: 8px;
|
row-gap: 8px;
|
||||||
|
|||||||
@ -1,10 +1,19 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { listEntryTemplates, loadEntryTemplate, type EntryTemplateItemDto } from "$lib/backend/templates";
|
import {
|
||||||
|
listEntryTemplates,
|
||||||
|
loadEntryTemplate,
|
||||||
|
type EntryTemplateItemDto,
|
||||||
|
} from "$lib/backend/templates";
|
||||||
import MarkdownToolbar from "$lib/components/editor/MarkdownToolbar.svelte";
|
import MarkdownToolbar from "$lib/components/editor/MarkdownToolbar.svelte";
|
||||||
import { entriesStore } from "$lib/stores/entries";
|
import { entriesStore } from "$lib/stores/entries";
|
||||||
import { fragmentsStore } from "$lib/stores/fragments";
|
import { fragmentsStore } from "$lib/stores/fragments";
|
||||||
import { listsStore } from "$lib/stores/lists";
|
import { listsStore } from "$lib/stores/lists";
|
||||||
import { serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
import {
|
||||||
|
serializeTodoList,
|
||||||
|
todoListsStore,
|
||||||
|
todosStore,
|
||||||
|
} from "$lib/stores/todos";
|
||||||
import { renderMarkdown, extractEditorTitle } from "$lib/utils/markdown";
|
import { renderMarkdown, extractEditorTitle } from "$lib/utils/markdown";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
@ -95,14 +104,20 @@
|
|||||||
applyWrap("[", "](https://example.com)");
|
applyWrap("[", "](https://example.com)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineMatchesListMode(line: string, mode: Exclude<ListMode, null>): boolean {
|
function lineMatchesListMode(
|
||||||
|
line: string,
|
||||||
|
mode: Exclude<ListMode, null>,
|
||||||
|
): boolean {
|
||||||
if (mode === "ul") {
|
if (mode === "ul") {
|
||||||
return /^\s*[-*+]\s/.test(line);
|
return /^\s*[-*+]\s/.test(line);
|
||||||
}
|
}
|
||||||
return /^\s*\d+\.\s/.test(line);
|
return /^\s*\d+\.\s/.test(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMarkerOnlyLine(line: string, mode: Exclude<ListMode, null>): boolean {
|
function isMarkerOnlyLine(
|
||||||
|
line: string,
|
||||||
|
mode: Exclude<ListMode, null>,
|
||||||
|
): boolean {
|
||||||
if (mode === "ul") {
|
if (mode === "ul") {
|
||||||
return /^\s*[-*+]\s$/.test(line);
|
return /^\s*[-*+]\s$/.test(line);
|
||||||
}
|
}
|
||||||
@ -178,7 +193,10 @@
|
|||||||
return value.replace(/]/g, "\\]");
|
return value.replace(/]/g, "\\]");
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendToAttachmentsSection(lineToAppend: string, attachmentId: string) {
|
function appendToAttachmentsSection(
|
||||||
|
lineToAppend: string,
|
||||||
|
attachmentId: string,
|
||||||
|
) {
|
||||||
const current = markdownText;
|
const current = markdownText;
|
||||||
const normalized = current.replace(/\r\n/g, "\n");
|
const normalized = current.replace(/\r\n/g, "\n");
|
||||||
if (normalized.includes(`(journal:${attachmentId})`)) {
|
if (normalized.includes(`(journal:${attachmentId})`)) {
|
||||||
@ -196,18 +214,25 @@
|
|||||||
const headerStart = headerMatch.index;
|
const headerStart = headerMatch.index;
|
||||||
const headerEnd = headerStart + headerMatch[0].length;
|
const headerEnd = headerStart + headerMatch[0].length;
|
||||||
const bodyStart = normalized.indexOf("\n", headerEnd);
|
const bodyStart = normalized.indexOf("\n", headerEnd);
|
||||||
const sectionBodyStart = bodyStart === -1 ? normalized.length : bodyStart + 1;
|
const sectionBodyStart =
|
||||||
|
bodyStart === -1 ? normalized.length : bodyStart + 1;
|
||||||
const nextHeaderMatch = /^##\s+/m.exec(normalized.slice(sectionBodyStart));
|
const nextHeaderMatch = /^##\s+/m.exec(normalized.slice(sectionBodyStart));
|
||||||
const sectionEnd = nextHeaderMatch ? sectionBodyStart + nextHeaderMatch.index : normalized.length;
|
const sectionEnd = nextHeaderMatch
|
||||||
|
? sectionBodyStart + nextHeaderMatch.index
|
||||||
|
: normalized.length;
|
||||||
|
|
||||||
const sectionBody = normalized.slice(sectionBodyStart, sectionEnd);
|
const sectionBody = normalized.slice(sectionBodyStart, sectionEnd);
|
||||||
const bodyPrefix = sectionBody.length > 0 && !sectionBody.endsWith("\n") ? "\n" : "";
|
const bodyPrefix =
|
||||||
|
sectionBody.length > 0 && !sectionBody.endsWith("\n") ? "\n" : "";
|
||||||
const insertion = `${bodyPrefix}${lineToAppend}\n`;
|
const insertion = `${bodyPrefix}${lineToAppend}\n`;
|
||||||
const next = `${normalized.slice(0, sectionEnd)}${insertion}${normalized.slice(sectionEnd)}`;
|
const next = `${normalized.slice(0, sectionEnd)}${insertion}${normalized.slice(sectionEnd)}`;
|
||||||
updateDraft(next);
|
updateDraft(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachReference(kind: "Fragment" | "List" | "To-Do", option: AttachmentOption) {
|
function attachReference(
|
||||||
|
kind: "Fragment" | "List" | "To-Do",
|
||||||
|
option: AttachmentOption,
|
||||||
|
) {
|
||||||
const label = escapeMarkdownLinkText(option.label.trim() || `${kind} Item`);
|
const label = escapeMarkdownLinkText(option.label.trim() || `${kind} Item`);
|
||||||
const line = `- ${kind}: [${label}](journal:${option.id})`;
|
const line = `- ${kind}: [${label}](journal:${option.id})`;
|
||||||
appendToAttachmentsSection(line, option.id);
|
appendToAttachmentsSection(line, option.id);
|
||||||
@ -221,7 +246,10 @@
|
|||||||
attachmentModalOpen = false;
|
attachmentModalOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachFromModal(kind: "Fragment" | "List" | "To-Do", option: AttachmentOption) {
|
function attachFromModal(
|
||||||
|
kind: "Fragment" | "List" | "To-Do",
|
||||||
|
option: AttachmentOption,
|
||||||
|
) {
|
||||||
attachReference(kind, option);
|
attachReference(kind, option);
|
||||||
attachmentModalOpen = false;
|
attachmentModalOpen = false;
|
||||||
}
|
}
|
||||||
@ -243,19 +271,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveJournalLinkTarget(targetId: string): { id: string; label: string; initialContent: string } | null {
|
function resolveJournalLinkTarget(
|
||||||
|
targetId: string,
|
||||||
|
): { id: string; label: string; initialContent: string } | null {
|
||||||
if (!targetId) return null;
|
if (!targetId) return null;
|
||||||
|
|
||||||
if (targetId.startsWith("fragments/")) {
|
if (targetId.startsWith("fragments/")) {
|
||||||
const fragment = get(fragmentsStore).find((item) => item.id === targetId);
|
const fragment = get(fragmentsStore).find((item) => item.id === targetId);
|
||||||
if (!fragment) return null;
|
if (!fragment) return null;
|
||||||
return { id: fragment.id, label: fragment.label, initialContent: fragment.initialContent };
|
return {
|
||||||
|
id: fragment.id,
|
||||||
|
label: fragment.label,
|
||||||
|
initialContent: fragment.initialContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetId.startsWith("lists/")) {
|
if (targetId.startsWith("lists/")) {
|
||||||
const list = get(listsStore).find((item) => item.id === targetId);
|
const list = get(listsStore).find((item) => item.id === targetId);
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
return { id: list.id, label: list.label, initialContent: list.initialContent };
|
return {
|
||||||
|
id: list.id,
|
||||||
|
label: list.label,
|
||||||
|
initialContent: list.initialContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetId.startsWith("todos/")) {
|
if (targetId.startsWith("todos/")) {
|
||||||
@ -265,14 +303,18 @@
|
|||||||
return {
|
return {
|
||||||
id: todoList.id,
|
id: todoList.id,
|
||||||
label: todoList.label,
|
label: todoList.label,
|
||||||
initialContent: serializeTodoList(todoList.label, todoItems)
|
initialContent: serializeTodoList(todoList.label, todoItems),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetId.startsWith("entries/")) {
|
if (targetId.startsWith("entries/")) {
|
||||||
const entry = get(entriesStore).find((item) => item.id === targetId);
|
const entry = get(entriesStore).find((item) => item.id === targetId);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
return { id: entry.id, label: entry.label, initialContent: entry.initialContent };
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
label: entry.label,
|
||||||
|
initialContent: entry.initialContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -295,8 +337,8 @@
|
|||||||
id: openDocumentId,
|
id: openDocumentId,
|
||||||
label: openDocumentName,
|
label: openDocumentName,
|
||||||
initialContent: openDocumentContent,
|
initialContent: openDocumentContent,
|
||||||
section: "entries"
|
section: "entries",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,7 +349,7 @@
|
|||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
node.removeEventListener("click", onClick);
|
node.removeEventListener("click", onClick);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,7 +469,9 @@
|
|||||||
lastOpenDocumentId = openDocumentId;
|
lastOpenDocumentId = openDocumentId;
|
||||||
listMode = null;
|
listMode = null;
|
||||||
}
|
}
|
||||||
$: isEntryDocument = openDocumentId.startsWith("entries/file/") || openDocumentId.startsWith("entries/draft-");
|
$: isEntryDocument =
|
||||||
|
openDocumentId.startsWith("entries/file/") ||
|
||||||
|
openDocumentId.startsWith("entries/draft-");
|
||||||
$: fragmentAttachmentOptions = $fragmentsStore
|
$: fragmentAttachmentOptions = $fragmentsStore
|
||||||
.filter((item) => item.id && item.label)
|
.filter((item) => item.id && item.label)
|
||||||
.map((item) => ({ id: item.id, label: item.label }));
|
.map((item) => ({ id: item.id, label: item.label }));
|
||||||
@ -452,7 +496,10 @@
|
|||||||
{templatesBusy}
|
{templatesBusy}
|
||||||
{templateOptions}
|
{templateOptions}
|
||||||
{listMode}
|
{listMode}
|
||||||
attachmentsDisabled={fragmentAttachmentOptions.length + listAttachmentOptions.length + todoAttachmentOptions.length === 0}
|
attachmentsDisabled={fragmentAttachmentOptions.length +
|
||||||
|
listAttachmentOptions.length +
|
||||||
|
todoAttachmentOptions.length ===
|
||||||
|
0}
|
||||||
onApplyHeading={applyHeading}
|
onApplyHeading={applyHeading}
|
||||||
onApplyTemplate={(filePath) => void applyTemplateByPath(filePath)}
|
onApplyTemplate={(filePath) => void applyTemplateByPath(filePath)}
|
||||||
onOpenAttachments={openAttachmentModal}
|
onOpenAttachments={openAttachmentModal}
|
||||||
@ -477,16 +524,30 @@
|
|||||||
|
|
||||||
{#if attachmentModalOpen}
|
{#if attachmentModalOpen}
|
||||||
<div class="attachment-modal-backdrop" role="presentation">
|
<div class="attachment-modal-backdrop" role="presentation">
|
||||||
<div class="attachment-modal" role="dialog" aria-modal="true" aria-label="Attach item">
|
<div
|
||||||
|
class="attachment-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Attach item"
|
||||||
|
>
|
||||||
<header class="attachment-modal-header">
|
<header class="attachment-modal-header">
|
||||||
<h2>Attach Item</h2>
|
<h2>Attach Item</h2>
|
||||||
<button type="button" class="attachment-modal-close" on:click={closeAttachmentModal} aria-label="Close attach dialog">
|
<button
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
type="button"
|
||||||
|
class="attachment-modal-close"
|
||||||
|
on:click={closeAttachmentModal}
|
||||||
|
aria-label="Close attach dialog"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if fragmentAttachmentOptions.length + listAttachmentOptions.length + todoAttachmentOptions.length === 0}
|
{#if fragmentAttachmentOptions.length + listAttachmentOptions.length + todoAttachmentOptions.length === 0}
|
||||||
<p class="attachment-empty">No fragments, lists, or to-do lists are available to attach.</p>
|
<p class="attachment-empty">
|
||||||
|
No fragments, lists, or to-do lists are available to attach.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#if fragmentAttachmentOptions.length > 0}
|
{#if fragmentAttachmentOptions.length > 0}
|
||||||
<div class="attachment-group">
|
<div class="attachment-group">
|
||||||
@ -496,7 +557,9 @@
|
|||||||
aria-label="Attach fragment"
|
aria-label="Attach fragment"
|
||||||
on:change={(event) => {
|
on:change={(event) => {
|
||||||
const target = event.currentTarget as HTMLSelectElement;
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
const selected = fragmentAttachmentOptions.find((option) => option.id === target.value);
|
const selected = fragmentAttachmentOptions.find(
|
||||||
|
(option) => option.id === target.value,
|
||||||
|
);
|
||||||
if (selected) attachFromModal("Fragment", selected);
|
if (selected) attachFromModal("Fragment", selected);
|
||||||
target.value = "";
|
target.value = "";
|
||||||
}}
|
}}
|
||||||
@ -517,7 +580,9 @@
|
|||||||
aria-label="Attach list"
|
aria-label="Attach list"
|
||||||
on:change={(event) => {
|
on:change={(event) => {
|
||||||
const target = event.currentTarget as HTMLSelectElement;
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
const selected = listAttachmentOptions.find((option) => option.id === target.value);
|
const selected = listAttachmentOptions.find(
|
||||||
|
(option) => option.id === target.value,
|
||||||
|
);
|
||||||
if (selected) attachFromModal("List", selected);
|
if (selected) attachFromModal("List", selected);
|
||||||
target.value = "";
|
target.value = "";
|
||||||
}}
|
}}
|
||||||
@ -538,7 +603,9 @@
|
|||||||
aria-label="Attach to-do list"
|
aria-label="Attach to-do list"
|
||||||
on:change={(event) => {
|
on:change={(event) => {
|
||||||
const target = event.currentTarget as HTMLSelectElement;
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
const selected = todoAttachmentOptions.find((option) => option.id === target.value);
|
const selected = todoAttachmentOptions.find(
|
||||||
|
(option) => option.id === target.value,
|
||||||
|
);
|
||||||
if (selected) attachFromModal("To-Do", selected);
|
if (selected) attachFromModal("To-Do", selected);
|
||||||
target.value = "";
|
target.value = "";
|
||||||
}}
|
}}
|
||||||
@ -557,7 +624,11 @@
|
|||||||
|
|
||||||
<div class="editor-workspace">
|
<div class="editor-workspace">
|
||||||
{#if previewOnly}
|
{#if previewOnly}
|
||||||
<article class="markdown-preview" aria-label="Markdown preview" use:interceptJournalLinks>
|
<article
|
||||||
|
class="markdown-preview"
|
||||||
|
aria-label="Markdown preview"
|
||||||
|
use:interceptJournalLinks
|
||||||
|
>
|
||||||
{@html renderedHtml}
|
{@html renderedHtml}
|
||||||
</article>
|
</article>
|
||||||
{:else}
|
{:else}
|
||||||
@ -679,7 +750,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: color-mix(in srgb, var(--surface-2) 90%, var(--bg-editor) 10%);
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--surface-2) 90%,
|
||||||
|
var(--bg-editor) 10%
|
||||||
|
);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 8px 34px 8px 10px;
|
padding: 8px 34px 8px 10px;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
@ -720,7 +795,9 @@
|
|||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
font-family: "Segoe UI Variable", "Segoe UI", "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", Inter, Roboto, Helvetica, Arial, sans-serif;
|
font-family:
|
||||||
|
"Segoe UI Variable", "Segoe UI", "Segoe UI Emoji", "Apple Color Emoji",
|
||||||
|
"Noto Color Emoji", Inter, Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-input {
|
.markdown-input {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { EntryTemplateItemDto } from "$lib/backend/templates";
|
import type { EntryTemplateItemDto } from "$lib/backend/templates";
|
||||||
type AttachmentOption = { id: string; label: string };
|
type AttachmentOption = { id: string; label: string };
|
||||||
@ -61,7 +62,11 @@
|
|||||||
|
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<div class="toolbar-select-wrap heading-wrap" bind:this={headingMenuEl} on:focusout={handleHeadingFocusOut}>
|
<div
|
||||||
|
class="toolbar-select-wrap heading-wrap"
|
||||||
|
bind:this={headingMenuEl}
|
||||||
|
on:focusout={handleHeadingFocusOut}
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">title</span>
|
<span class="material-symbols-outlined" aria-hidden="true">title</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -72,22 +77,54 @@
|
|||||||
aria-expanded={headingMenuOpen}
|
aria-expanded={headingMenuOpen}
|
||||||
on:click={toggleHeadingMenu}
|
on:click={toggleHeadingMenu}
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">expand_more</span>
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>expand_more</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
{#if headingMenuOpen}
|
{#if headingMenuOpen}
|
||||||
<div class="heading-menu" role="listbox" aria-label="Header size">
|
<div class="heading-menu" role="listbox" aria-label="Header size">
|
||||||
<button type="button" class="heading-option" on:click={() => selectHeading(1)}>H1</button>
|
<button
|
||||||
<button type="button" class="heading-option" on:click={() => selectHeading(2)}>H2</button>
|
type="button"
|
||||||
<button type="button" class="heading-option" on:click={() => selectHeading(3)}>H3</button>
|
class="heading-option"
|
||||||
<button type="button" class="heading-option" on:click={() => selectHeading(4)}>H4</button>
|
on:click={() => selectHeading(1)}>H1</button
|
||||||
<button type="button" class="heading-option" on:click={() => selectHeading(5)}>H5</button>
|
>
|
||||||
<button type="button" class="heading-option" on:click={() => selectHeading(6)}>H6</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="heading-option"
|
||||||
|
on:click={() => selectHeading(2)}>H2</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="heading-option"
|
||||||
|
on:click={() => selectHeading(3)}>H3</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="heading-option"
|
||||||
|
on:click={() => selectHeading(4)}>H4</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="heading-option"
|
||||||
|
on:click={() => selectHeading(5)}>H5</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="heading-option"
|
||||||
|
on:click={() => selectHeading(6)}>H6</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isEntryDocument}
|
{#if isEntryDocument}
|
||||||
<div class="toolbar-select-wrap template-wrap" bind:this={templateMenuEl} on:focusout={handleTemplateFocusOut}>
|
<div
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">description</span>
|
class="toolbar-select-wrap template-wrap"
|
||||||
|
bind:this={templateMenuEl}
|
||||||
|
on:focusout={handleTemplateFocusOut}
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>description</span
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="template-trigger"
|
class="template-trigger"
|
||||||
@ -98,8 +135,12 @@
|
|||||||
disabled={templatesBusy}
|
disabled={templatesBusy}
|
||||||
on:click={toggleTemplateMenu}
|
on:click={toggleTemplateMenu}
|
||||||
>
|
>
|
||||||
<span class="template-trigger-text">{templatesBusy ? "Loading..." : "Template"}</span>
|
<span class="template-trigger-text"
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">expand_more</span>
|
>{templatesBusy ? "Loading..." : "Template"}</span
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>expand_more</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
{#if templateMenuOpen}
|
{#if templateMenuOpen}
|
||||||
<div class="template-menu" role="listbox" aria-label="Template">
|
<div class="template-menu" role="listbox" aria-label="Template">
|
||||||
@ -134,19 +175,55 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="toolbar-divider" aria-hidden="true"></div>
|
<div class="toolbar-divider" aria-hidden="true"></div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button type="button" class="toolbar-btn toolbar-icon-btn" on:click={onBold} aria-label="Bold" title="Bold">
|
<button
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">format_bold</span>
|
type="button"
|
||||||
|
class="toolbar-btn toolbar-icon-btn"
|
||||||
|
on:click={onBold}
|
||||||
|
aria-label="Bold"
|
||||||
|
title="Bold"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>format_bold</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="toolbar-btn toolbar-icon-btn" on:click={onItalic} aria-label="Italic" title="Italic">
|
<button
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">format_italic</span>
|
type="button"
|
||||||
|
class="toolbar-btn toolbar-icon-btn"
|
||||||
|
on:click={onItalic}
|
||||||
|
aria-label="Italic"
|
||||||
|
title="Italic"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>format_italic</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="toolbar-btn toolbar-icon-btn" on:click={onUnderline} aria-label="Underline" title="Underline">
|
<button
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">format_underlined</span>
|
type="button"
|
||||||
|
class="toolbar-btn toolbar-icon-btn"
|
||||||
|
on:click={onUnderline}
|
||||||
|
aria-label="Underline"
|
||||||
|
title="Underline"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>format_underlined</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="toolbar-btn toolbar-icon-btn" on:click={onTag} aria-label="Tag" title="Tag [[...]]">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toolbar-btn toolbar-icon-btn"
|
||||||
|
on:click={onTag}
|
||||||
|
aria-label="Tag"
|
||||||
|
title="Tag [[...]]"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">sell</span>
|
<span class="material-symbols-outlined" aria-hidden="true">sell</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="toolbar-btn toolbar-icon-btn" on:click={onLink} aria-label="Link" title="Link">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toolbar-btn toolbar-icon-btn"
|
||||||
|
on:click={onLink}
|
||||||
|
aria-label="Link"
|
||||||
|
title="Link"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">link</span>
|
<span class="material-symbols-outlined" aria-hidden="true">link</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -157,7 +234,9 @@
|
|||||||
aria-label="Bulleted list"
|
aria-label="Bulleted list"
|
||||||
title="Bulleted list"
|
title="Bulleted list"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">format_list_bulleted</span>
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>format_list_bulleted</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -167,9 +246,17 @@
|
|||||||
aria-label="Numbered list"
|
aria-label="Numbered list"
|
||||||
title="Numbered list"
|
title="Numbered list"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">format_list_numbered</span>
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
|
>format_list_numbered</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="toolbar-btn toolbar-icon-btn" on:click={onCode} aria-label="Inline code" title="Inline code">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toolbar-btn toolbar-icon-btn"
|
||||||
|
on:click={onCode}
|
||||||
|
aria-label="Inline code"
|
||||||
|
title="Inline code"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">code</span>
|
<span class="material-symbols-outlined" aria-hidden="true">code</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -245,7 +332,11 @@
|
|||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
transition:
|
||||||
|
background-color 120ms ease,
|
||||||
|
color 120ms ease,
|
||||||
|
border-color 120ms ease,
|
||||||
|
transform 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-icon-btn {
|
.toolbar-icon-btn {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
addTodoItem,
|
addTodoItem,
|
||||||
@ -12,7 +13,7 @@
|
|||||||
todosStore,
|
todosStore,
|
||||||
type TodoItem,
|
type TodoItem,
|
||||||
updateTodoItemText,
|
updateTodoItemText,
|
||||||
updateTodoItemTextBackend
|
updateTodoItemTextBackend,
|
||||||
} from "$lib/stores/todos";
|
} from "$lib/stores/todos";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
@ -45,7 +46,9 @@
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
todoItems = toggleTodoItem(todoItems, id);
|
todoItems = toggleTodoItem(todoItems, id);
|
||||||
} else {
|
} else {
|
||||||
todoItems = todoItems.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
|
todoItems = todoItems.map((t) =>
|
||||||
|
t.id === id ? { ...t, done: !t.done } : t,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
persistTodosForCurrentList();
|
persistTodosForCurrentList();
|
||||||
}
|
}
|
||||||
@ -68,7 +71,9 @@
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
todoItems = updateTodoItemText(todoItems, id, text);
|
todoItems = updateTodoItemText(todoItems, id, text);
|
||||||
} else {
|
} else {
|
||||||
todoItems = todoItems.map((t) => (t.id === id ? { ...t, text: text.trim() } : t));
|
todoItems = todoItems.map((t) =>
|
||||||
|
t.id === id ? { ...t, text: text.trim() } : t,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
persistTodosForCurrentList();
|
persistTodosForCurrentList();
|
||||||
}
|
}
|
||||||
@ -138,7 +143,11 @@
|
|||||||
{#each todoItems as todo}
|
{#each todoItems as todo}
|
||||||
<li class="todo-item">
|
<li class="todo-item">
|
||||||
<label class="todo-check">
|
<label class="todo-check">
|
||||||
<input type="checkbox" checked={todo.done} on:change={() => toggleTodoDone(todo.id)} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.done}
|
||||||
|
on:change={() => toggleTodoDone(todo.id)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if editingTodoId === todo.id}
|
{#if editingTodoId === todo.id}
|
||||||
@ -152,14 +161,30 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="todo-actions">
|
<div class="todo-actions">
|
||||||
<button type="button" class="todo-btn save" on:click={saveEditTodo}>Save</button>
|
<button
|
||||||
<button type="button" class="todo-btn ghost" on:click={cancelEditTodo}>Cancel</button>
|
type="button"
|
||||||
|
class="todo-btn save"
|
||||||
|
on:click={saveEditTodo}>Save</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="todo-btn ghost"
|
||||||
|
on:click={cancelEditTodo}>Cancel</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="todo-text" class:is-done={todo.done}>{todo.text}</span>
|
<span class="todo-text" class:is-done={todo.done}>{todo.text}</span>
|
||||||
<div class="todo-actions">
|
<div class="todo-actions">
|
||||||
<button type="button" class="todo-btn ghost" on:click={() => startEditTodo(todo.id)}>Edit</button>
|
<button
|
||||||
<button type="button" class="todo-btn danger" on:click={() => removeTodo(todo.id)}>Remove</button>
|
type="button"
|
||||||
|
class="todo-btn ghost"
|
||||||
|
on:click={() => startEditTodo(todo.id)}>Edit</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="todo-btn danger"
|
||||||
|
on:click={() => removeTodo(todo.id)}>Remove</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
@ -293,6 +318,14 @@
|
|||||||
padding: 18px 16px;
|
padding: 18px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.todo-create {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.todo-item {
|
.todo-item {
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
row-gap: 8px;
|
row-gap: 8px;
|
||||||
|
|||||||
@ -32,7 +32,10 @@ export function isTauriRuntime(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.prototype.hasOwnProperty.call(window as WindowWithTauri, "__TAURI_INTERNALS__");
|
return Object.prototype.hasOwnProperty.call(
|
||||||
|
window as WindowWithTauri,
|
||||||
|
"__TAURI_INTERNALS__",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readUiSettingsFromLocalStorage(): UiSettingsPayload {
|
function readUiSettingsFromLocalStorage(): UiSettingsPayload {
|
||||||
@ -49,8 +52,13 @@ function readUiSettingsFromLocalStorage(): UiSettingsPayload {
|
|||||||
const parsed = JSON.parse(raw) as UiSettingsPayload;
|
const parsed = JSON.parse(raw) as UiSettingsPayload;
|
||||||
return {
|
return {
|
||||||
tags: Array.isArray(parsed.tags) ? parsed.tags : undefined,
|
tags: Array.isArray(parsed.tags) ? parsed.tags : undefined,
|
||||||
fragmentTypes: Array.isArray(parsed.fragmentTypes) ? parsed.fragmentTypes : undefined,
|
fragmentTypes: Array.isArray(parsed.fragmentTypes)
|
||||||
defaultStartupView: typeof parsed.defaultStartupView === "string" ? parsed.defaultStartupView : undefined
|
? parsed.fragmentTypes
|
||||||
|
: undefined,
|
||||||
|
defaultStartupView:
|
||||||
|
typeof parsed.defaultStartupView === "string"
|
||||||
|
? parsed.defaultStartupView
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
@ -64,21 +72,30 @@ function writeUiSettingsToLocalStorage(payload: UiSettingsPayload): void {
|
|||||||
|
|
||||||
const safePayload: UiSettingsPayload = {
|
const safePayload: UiSettingsPayload = {
|
||||||
tags: Array.isArray(payload.tags) ? payload.tags : undefined,
|
tags: Array.isArray(payload.tags) ? payload.tags : undefined,
|
||||||
fragmentTypes: Array.isArray(payload.fragmentTypes) ? payload.fragmentTypes : undefined,
|
fragmentTypes: Array.isArray(payload.fragmentTypes)
|
||||||
defaultStartupView: typeof payload.defaultStartupView === "string" ? payload.defaultStartupView : undefined
|
? payload.fragmentTypes
|
||||||
|
: undefined,
|
||||||
|
defaultStartupView:
|
||||||
|
typeof payload.defaultStartupView === "string"
|
||||||
|
? payload.defaultStartupView
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.localStorage.setItem(UI_SETTINGS_KEY, JSON.stringify(safePayload));
|
window.localStorage.setItem(UI_SETTINGS_KEY, JSON.stringify(safePayload));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson<T>(path: string, init: RequestInit = {}, options: FetchJsonOptions = {}): Promise<T> {
|
async function fetchJson<T>(
|
||||||
|
path: string,
|
||||||
|
init: RequestInit = {},
|
||||||
|
options: FetchJsonOptions = {},
|
||||||
|
): Promise<T> {
|
||||||
const response = await fetch(`${normalizedApiBase()}${path}`, {
|
const response = await fetch(`${normalizedApiBase()}${path}`, {
|
||||||
...init,
|
...init,
|
||||||
keepalive: options.keepalive === true,
|
keepalive: options.keepalive === true,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(init.headers ?? {})
|
...(init.headers ?? {}),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -93,7 +110,10 @@ async function fetchJson<T>(path: string, init: RequestInit = {}, options: Fetch
|
|||||||
return (await response.json()) as T;
|
return (await response.json()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function invoke<T>(command: string, args?: InvokeArgs): Promise<T> {
|
export async function invoke<T>(
|
||||||
|
command: string,
|
||||||
|
args?: InvokeArgs,
|
||||||
|
): Promise<T> {
|
||||||
if (isTauriRuntime()) {
|
if (isTauriRuntime()) {
|
||||||
const tauriCore = await import("@tauri-apps/api/core");
|
const tauriCore = await import("@tauri-apps/api/core");
|
||||||
return tauriCore.invoke<T>(command, args);
|
return tauriCore.invoke<T>(command, args);
|
||||||
@ -112,9 +132,9 @@ export async function invoke<T>(command: string, args?: InvokeArgs): Promise<T>
|
|||||||
"/command",
|
"/command",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(envelope as BackendCommand)
|
body: JSON.stringify(envelope as BackendCommand),
|
||||||
},
|
},
|
||||||
{ keepalive }
|
{ keepalive },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "get_sidecar_root":
|
case "get_sidecar_root":
|
||||||
@ -123,23 +143,32 @@ export async function invoke<T>(command: string, args?: InvokeArgs): Promise<T>
|
|||||||
const path = typeof args?.path === "string" ? args.path : "";
|
const path = typeof args?.path === "string" ? args.path : "";
|
||||||
return fetchJson<T>("/sidecar/root", {
|
return fetchJson<T>("/sidecar/root", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ path })
|
body: JSON.stringify({ path }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case "get_ui_settings":
|
case "get_ui_settings":
|
||||||
return readUiSettingsFromLocalStorage() as T;
|
return readUiSettingsFromLocalStorage() as T;
|
||||||
case "set_ui_settings": {
|
case "set_ui_settings": {
|
||||||
const tags = Array.isArray(args?.tags) ? (args?.tags as string[]) : undefined;
|
const tags = Array.isArray(args?.tags)
|
||||||
const fragmentTypes =
|
? (args?.tags as string[])
|
||||||
Array.isArray(args?.fragmentTypes) ? (args?.fragmentTypes as string[]) :
|
: undefined;
|
||||||
Array.isArray(args?.fragment_types) ? (args?.fragment_types as string[]) :
|
const fragmentTypes = Array.isArray(args?.fragmentTypes)
|
||||||
undefined;
|
? (args?.fragmentTypes as string[])
|
||||||
|
: Array.isArray(args?.fragment_types)
|
||||||
|
? (args?.fragment_types as string[])
|
||||||
|
: undefined;
|
||||||
const defaultStartupView =
|
const defaultStartupView =
|
||||||
typeof args?.defaultStartupView === "string" ? args.defaultStartupView :
|
typeof args?.defaultStartupView === "string"
|
||||||
typeof args?.default_startup_view === "string" ? args.default_startup_view :
|
? args.defaultStartupView
|
||||||
undefined;
|
: typeof args?.default_startup_view === "string"
|
||||||
|
? args.default_startup_view
|
||||||
|
: undefined;
|
||||||
|
|
||||||
writeUiSettingsToLocalStorage({ tags, fragmentTypes, defaultStartupView });
|
writeUiSettingsToLocalStorage({
|
||||||
|
tags,
|
||||||
|
fragmentTypes,
|
||||||
|
defaultStartupView,
|
||||||
|
});
|
||||||
return undefined as T;
|
return undefined as T;
|
||||||
}
|
}
|
||||||
case "shutdown":
|
case "shutdown":
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
saveEntry as saveEntryCommand,
|
saveEntry as saveEntryCommand,
|
||||||
searchEntries as searchEntriesCommand,
|
searchEntries as searchEntriesCommand,
|
||||||
type EntryListItemDto,
|
type EntryListItemDto,
|
||||||
type EntrySearchRequestDto
|
type EntrySearchRequestDto,
|
||||||
} from "$lib/backend/entries";
|
} from "$lib/backend/entries";
|
||||||
|
|
||||||
export type EntryItem = {
|
export type EntryItem = {
|
||||||
@ -59,17 +59,19 @@ function fromListDto(dto: EntryListItemDto): EntryItem {
|
|||||||
id: toStoreId(dto.filePath),
|
id: toStoreId(dto.filePath),
|
||||||
label: toLabel(dto.fileName),
|
label: toLabel(dto.fileName),
|
||||||
initialContent: "",
|
initialContent: "",
|
||||||
filePath: dto.filePath
|
filePath: dto.filePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromLoadResult(result: Awaited<ReturnType<typeof loadEntryCommand>>): EntryItem {
|
function fromLoadResult(
|
||||||
|
result: Awaited<ReturnType<typeof loadEntryCommand>>,
|
||||||
|
): EntryItem {
|
||||||
return {
|
return {
|
||||||
id: toStoreId(result.filePath),
|
id: toStoreId(result.filePath),
|
||||||
label: toLabel(result.fileName),
|
label: toLabel(result.fileName),
|
||||||
initialContent: result.entry.rawContent,
|
initialContent: result.entry.rawContent,
|
||||||
filePath: result.filePath,
|
filePath: result.filePath,
|
||||||
date: result.entry.date
|
date: result.entry.date,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +84,7 @@ export function createEntryDraft(): EntryItem {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: "Untitled Entry",
|
label: "Untitled Entry",
|
||||||
initialContent: "# Untitled Entry\n\nStart writing..."
|
initialContent: "# Untitled Entry\n\nStart writing...",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +102,9 @@ export async function hydrateEntries(dataDirectory?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadEntryByStoreId(storeId: string): Promise<EntryItem | null> {
|
export async function loadEntryByStoreId(
|
||||||
|
storeId: string,
|
||||||
|
): Promise<EntryItem | null> {
|
||||||
const filePath = toBackendPath(storeId);
|
const filePath = toBackendPath(storeId);
|
||||||
if (!filePath) return null;
|
if (!filePath) return null;
|
||||||
|
|
||||||
@ -115,12 +119,21 @@ export async function loadEntryByStoreId(storeId: string): Promise<EntryItem | n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveEntryFromStore(storeId: string, content: string, mode?: string): Promise<EntryItem | null> {
|
export async function saveEntryFromStore(
|
||||||
|
storeId: string,
|
||||||
|
content: string,
|
||||||
|
mode?: string,
|
||||||
|
): Promise<EntryItem | null> {
|
||||||
const trimmed = content?.trim();
|
const trimmed = content?.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
|
|
||||||
const existingPath = toBackendPath(storeId);
|
const existingPath = toBackendPath(storeId);
|
||||||
let payload: { content: string; filePath?: string; mode?: string; fileName?: string };
|
let payload: {
|
||||||
|
content: string;
|
||||||
|
filePath?: string;
|
||||||
|
mode?: string;
|
||||||
|
fileName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
if (existingPath) {
|
if (existingPath) {
|
||||||
payload = { content: trimmed, filePath: existingPath, mode };
|
payload = { content: trimmed, filePath: existingPath, mode };
|
||||||
@ -133,7 +146,9 @@ export async function saveEntryFromStore(storeId: string, content: string, mode?
|
|||||||
const loaded = await loadEntryCommand(saved.filePath);
|
const loaded = await loadEntryCommand(saved.filePath);
|
||||||
const item = fromLoadResult(loaded);
|
const item = fromLoadResult(loaded);
|
||||||
entriesStore.update((items) => {
|
entriesStore.update((items) => {
|
||||||
const filtered = existingPath ? items : items.filter((i) => i.id !== storeId);
|
const filtered = existingPath
|
||||||
|
? items
|
||||||
|
: items.filter((i) => i.id !== storeId);
|
||||||
return upsertById(filtered, item);
|
return upsertById(filtered, item);
|
||||||
});
|
});
|
||||||
return item;
|
return item;
|
||||||
@ -143,7 +158,9 @@ export async function saveEntryFromStore(storeId: string, content: string, mode?
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchEntriesAsItems(payload: EntrySearchRequestDto): Promise<EntryItem[]> {
|
export async function searchEntriesAsItems(
|
||||||
|
payload: EntrySearchRequestDto,
|
||||||
|
): Promise<EntryItem[]> {
|
||||||
const results = await searchEntriesCommand(payload);
|
const results = await searchEntriesCommand(payload);
|
||||||
const dataDirectory = payload.dataDirectory?.trim() ?? "";
|
const dataDirectory = payload.dataDirectory?.trim() ?? "";
|
||||||
const separator = dataDirectory.includes("\\") ? "\\" : "/";
|
const separator = dataDirectory.includes("\\") ? "\\" : "/";
|
||||||
@ -154,8 +171,10 @@ export async function searchEntriesAsItems(payload: EntrySearchRequestDto): Prom
|
|||||||
: `entries/search/${encodeURIComponent(result.fileName)}`,
|
: `entries/search/${encodeURIComponent(result.fileName)}`,
|
||||||
label: toLabel(result.fileName),
|
label: toLabel(result.fileName),
|
||||||
initialContent: result.entry.rawContent,
|
initialContent: result.entry.rawContent,
|
||||||
filePath: basePath ? `${basePath}${separator}${result.fileName}` : undefined,
|
filePath: basePath
|
||||||
date: result.entry.date
|
? `${basePath}${separator}${result.fileName}`
|
||||||
|
: undefined,
|
||||||
|
date: result.entry.date,
|
||||||
}));
|
}));
|
||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
deleteFragment as deleteFragmentCommand,
|
deleteFragment as deleteFragmentCommand,
|
||||||
listFragments,
|
listFragments,
|
||||||
updateFragment as updateFragmentCommand,
|
updateFragment as updateFragmentCommand,
|
||||||
type FragmentDto
|
type FragmentDto,
|
||||||
} from "$lib/backend/fragments";
|
} from "$lib/backend/fragments";
|
||||||
|
|
||||||
export type FragmentItem = {
|
export type FragmentItem = {
|
||||||
@ -36,7 +36,10 @@ function toBackendId(id: string): string | null {
|
|||||||
return backendId || null;
|
return backendId || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitDescription(description: string): { title: string; body: string } {
|
function splitDescription(description: string): {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
} {
|
||||||
const normalized = description.trim();
|
const normalized = description.trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return { title: "Untitled Fragment", body: "" };
|
return { title: "Untitled Fragment", body: "" };
|
||||||
@ -67,8 +70,8 @@ function dtoToItem(dto: FragmentDto): FragmentItem {
|
|||||||
title: parsed.title,
|
title: parsed.title,
|
||||||
type: dto.type,
|
type: dto.type,
|
||||||
tags: dto.tags ?? [],
|
tags: dto.tags ?? [],
|
||||||
body: parsed.body
|
body: parsed.body,
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,12 +98,17 @@ export function createFragmentId(title: string): string {
|
|||||||
export function serializeFragment(payload: ParsedFragment): string {
|
export function serializeFragment(payload: ParsedFragment): string {
|
||||||
const title = payload.title.trim() || "Untitled Fragment";
|
const title = payload.title.trim() || "Untitled Fragment";
|
||||||
const type = payload.type.trim();
|
const type = payload.type.trim();
|
||||||
const tagsLine = payload.tags.length ? payload.tags.map((tag) => `#${tag}`).join(" ") : "(none)";
|
const tagsLine = payload.tags.length
|
||||||
|
? payload.tags.map((tag) => `#${tag}`).join(" ")
|
||||||
|
: "(none)";
|
||||||
const body = payload.body.trim() || "Add details for this fragment.";
|
const body = payload.body.trim() || "Add details for this fragment.";
|
||||||
return `# ${title}\n\nType: ${type}\nTags: ${tagsLine}\n\n${body}`;
|
return `# ${title}\n\nType: ${type}\nTags: ${tagsLine}\n\n${body}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFragmentContent(content: string, fallbackTitle = "Untitled Fragment"): ParsedFragment {
|
export function parseFragmentContent(
|
||||||
|
content: string,
|
||||||
|
fallbackTitle = "Untitled Fragment",
|
||||||
|
): ParsedFragment {
|
||||||
const headingMatch = content.match(/^#\s+(.+)$/m);
|
const headingMatch = content.match(/^#\s+(.+)$/m);
|
||||||
const typeMatch = content.match(/^Type:\s*(.+)$/m);
|
const typeMatch = content.match(/^Type:\s*(.+)$/m);
|
||||||
const tagsMatch = content.match(/^Tags:\s*(.+)$/m);
|
const tagsMatch = content.match(/^Tags:\s*(.+)$/m);
|
||||||
@ -119,7 +127,7 @@ export function parseFragmentContent(content: string, fallbackTitle = "Untitled
|
|||||||
title: headingMatch?.[1]?.trim() || fallbackTitle,
|
title: headingMatch?.[1]?.trim() || fallbackTitle,
|
||||||
type: typeMatch?.[1]?.trim() || "",
|
type: typeMatch?.[1]?.trim() || "",
|
||||||
tags,
|
tags,
|
||||||
body: bodyMatch?.[1]?.trim() || ""
|
body: bodyMatch?.[1]?.trim() || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,31 +136,49 @@ export function createFragmentDraft(): FragmentItem {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: "New Fragment",
|
label: "New Fragment",
|
||||||
initialContent: "# New Fragment\n\nType: \nTags: (none)\n\n"
|
initialContent: "# New Fragment\n\nType: \nTags: (none)\n\n",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFragmentItem(title: string, content: string): FragmentItem {
|
export function createFragmentItem(
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
): FragmentItem {
|
||||||
return {
|
return {
|
||||||
id: createFragmentId(title),
|
id: createFragmentId(title),
|
||||||
label: title.trim() || "Untitled Fragment",
|
label: title.trim() || "Untitled Fragment",
|
||||||
initialContent: content
|
initialContent: content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateFragmentItem(items: FragmentItem[], id: string, title: string, content: string): FragmentItem[] {
|
export function updateFragmentItem(
|
||||||
|
items: FragmentItem[],
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
): FragmentItem[] {
|
||||||
return items.map((item) =>
|
return items.map((item) =>
|
||||||
item.id === id
|
item.id === id
|
||||||
? { ...item, label: title.trim() || "Untitled Fragment", initialContent: content }
|
? {
|
||||||
: item
|
...item,
|
||||||
|
label: title.trim() || "Untitled Fragment",
|
||||||
|
initialContent: content,
|
||||||
|
}
|
||||||
|
: item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prependFragmentItem(items: FragmentItem[], item: FragmentItem): FragmentItem[] {
|
export function prependFragmentItem(
|
||||||
|
items: FragmentItem[],
|
||||||
|
item: FragmentItem,
|
||||||
|
): FragmentItem[] {
|
||||||
return [item, ...items];
|
return [item, ...items];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeFragmentItem(items: FragmentItem[], id: string): FragmentItem[] {
|
export function removeFragmentItem(
|
||||||
|
items: FragmentItem[],
|
||||||
|
id: string,
|
||||||
|
): FragmentItem[] {
|
||||||
return items.filter((item) => item.id !== id);
|
return items.filter((item) => item.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,38 +195,45 @@ export async function hydrateFragments(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFragmentFromParsed(payload: ParsedFragment): Promise<FragmentItem> {
|
export async function createFragmentFromParsed(
|
||||||
|
payload: ParsedFragment,
|
||||||
|
): Promise<FragmentItem> {
|
||||||
const created = await createFragmentCommand({
|
const created = await createFragmentCommand({
|
||||||
type: payload.type.trim(),
|
type: payload.type.trim(),
|
||||||
description: composeDescription(payload.title, payload.body),
|
description: composeDescription(payload.title, payload.body),
|
||||||
tags: payload.tags
|
tags: payload.tags,
|
||||||
});
|
});
|
||||||
const item = dtoToItem(created);
|
const item = dtoToItem(created);
|
||||||
fragmentsStore.update((items) => prependFragmentItem(items, item));
|
fragmentsStore.update((items) => prependFragmentItem(items, item));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateFragmentFromParsed(storeId: string, payload: ParsedFragment): Promise<FragmentItem | null> {
|
export async function updateFragmentFromParsed(
|
||||||
|
storeId: string,
|
||||||
|
payload: ParsedFragment,
|
||||||
|
): Promise<FragmentItem | null> {
|
||||||
const backendId = toBackendId(storeId);
|
const backendId = toBackendId(storeId);
|
||||||
if (!backendId) return null;
|
if (!backendId) return null;
|
||||||
|
|
||||||
const ok = await updateFragmentCommand(backendId, {
|
const ok = await updateFragmentCommand(backendId, {
|
||||||
type: payload.type.trim(),
|
type: payload.type.trim(),
|
||||||
description: composeDescription(payload.title, payload.body),
|
description: composeDescription(payload.title, payload.body),
|
||||||
tags: payload.tags
|
tags: payload.tags,
|
||||||
});
|
});
|
||||||
if (!ok) return null;
|
if (!ok) return null;
|
||||||
|
|
||||||
const item: FragmentItem = {
|
const item: FragmentItem = {
|
||||||
id: storeId,
|
id: storeId,
|
||||||
label: payload.title.trim() || "Untitled Fragment",
|
label: payload.title.trim() || "Untitled Fragment",
|
||||||
initialContent: serializeFragment(payload)
|
initialContent: serializeFragment(payload),
|
||||||
};
|
};
|
||||||
fragmentsStore.update((items) => upsertById(items, item));
|
fragmentsStore.update((items) => upsertById(items, item));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFragmentByStoreId(storeId: string): Promise<boolean> {
|
export async function deleteFragmentByStoreId(
|
||||||
|
storeId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
const backendId = toBackendId(storeId);
|
const backendId = toBackendId(storeId);
|
||||||
if (!backendId) return false;
|
if (!backendId) return false;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
deleteList as deleteListCommand,
|
deleteList as deleteListCommand,
|
||||||
listLists,
|
listLists,
|
||||||
updateList as updateListCommand,
|
updateList as updateListCommand,
|
||||||
type ListDocumentDto
|
type ListDocumentDto,
|
||||||
} from "$lib/backend/lists";
|
} from "$lib/backend/lists";
|
||||||
|
|
||||||
export type ListItem = {
|
export type ListItem = {
|
||||||
@ -31,7 +31,7 @@ function dtoToItem(dto: ListDocumentDto): ListItem {
|
|||||||
return {
|
return {
|
||||||
id: toStoreId(dto.id),
|
id: toStoreId(dto.id),
|
||||||
label: dto.label,
|
label: dto.label,
|
||||||
initialContent: dto.content || `# ${dto.label}\n\n`
|
initialContent: dto.content || `# ${dto.label}\n\n`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export function createListDraft(): ListItem {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: "Untitled List",
|
label: "Untitled List",
|
||||||
initialContent: "# Untitled List\n\n- Item 1"
|
initialContent: "# Untitled List\n\n- Item 1",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,10 +65,16 @@ export async function hydrateLists(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createListFromLabel(label: string, content = ""): Promise<ListItem> {
|
export async function createListFromLabel(
|
||||||
|
label: string,
|
||||||
|
content = "",
|
||||||
|
): Promise<ListItem> {
|
||||||
const resolvedLabel = label.trim() || "Untitled List";
|
const resolvedLabel = label.trim() || "Untitled List";
|
||||||
const resolvedContent = content || `# ${resolvedLabel}\n\n`;
|
const resolvedContent = content || `# ${resolvedLabel}\n\n`;
|
||||||
const created = await createListCommand({ label: resolvedLabel, content: resolvedContent });
|
const created = await createListCommand({
|
||||||
|
label: resolvedLabel,
|
||||||
|
content: resolvedContent,
|
||||||
|
});
|
||||||
const item = dtoToItem(created);
|
const item = dtoToItem(created);
|
||||||
listsStore.update((items) => [item, ...items]);
|
listsStore.update((items) => [item, ...items]);
|
||||||
return item;
|
return item;
|
||||||
@ -77,7 +83,7 @@ export async function createListFromLabel(label: string, content = ""): Promise<
|
|||||||
export async function updateListByStoreId(
|
export async function updateListByStoreId(
|
||||||
storeId: string,
|
storeId: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
content?: string
|
content?: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const backendId = toBackendId(storeId);
|
const backendId = toBackendId(storeId);
|
||||||
if (!backendId) return false;
|
if (!backendId) return false;
|
||||||
@ -95,10 +101,10 @@ export async function updateListByStoreId(
|
|||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
label: label ?? item.label,
|
label: label ?? item.label,
|
||||||
initialContent: content ?? item.initialContent
|
initialContent: content ?? item.initialContent,
|
||||||
}
|
}
|
||||||
: item
|
: item,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,22 @@ import { invoke } from "$lib/runtime/invoke";
|
|||||||
|
|
||||||
const defaultTags = ["Personal", "Work", "Ideas", "Journal"];
|
const defaultTags = ["Personal", "Work", "Ideas", "Journal"];
|
||||||
const defaultFragmentTypes = ["Quote", "Snippet", "Reference"];
|
const defaultFragmentTypes = ["Quote", "Snippet", "Reference"];
|
||||||
const startupViews = ["entries", "calendar", "fragments", "todos", "lists"] as const;
|
const startupViews = [
|
||||||
|
"entries",
|
||||||
|
"calendar",
|
||||||
|
"fragments",
|
||||||
|
"todos",
|
||||||
|
"lists",
|
||||||
|
] as const;
|
||||||
const defaultStartupView = "entries";
|
const defaultStartupView = "entries";
|
||||||
export type StartupView = typeof startupViews[number];
|
export type StartupView = (typeof startupViews)[number];
|
||||||
|
|
||||||
export const settingsTags = writable<string[]>([...defaultTags]);
|
export const settingsTags = writable<string[]>([...defaultTags]);
|
||||||
export const settingsFragmentTypes = writable<string[]>([...defaultFragmentTypes]);
|
export const settingsFragmentTypes = writable<string[]>([
|
||||||
export const settingsDefaultStartupView = writable<StartupView>(defaultStartupView);
|
...defaultFragmentTypes,
|
||||||
|
]);
|
||||||
|
export const settingsDefaultStartupView =
|
||||||
|
writable<StartupView>(defaultStartupView);
|
||||||
|
|
||||||
let hydrationComplete = false;
|
let hydrationComplete = false;
|
||||||
let hydrating = false;
|
let hydrating = false;
|
||||||
@ -25,9 +34,15 @@ function normalize(value: string): string {
|
|||||||
return value.trim().toLowerCase();
|
return value.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasDuplicate(values: string[], candidate: string, excludeIndex?: number): boolean {
|
function hasDuplicate(
|
||||||
|
values: string[],
|
||||||
|
candidate: string,
|
||||||
|
excludeIndex?: number,
|
||||||
|
): boolean {
|
||||||
const normalized = normalize(candidate);
|
const normalized = normalize(candidate);
|
||||||
return values.some((value, index) => index !== excludeIndex && normalize(value) === normalized);
|
return values.some(
|
||||||
|
(value, index) => index !== excludeIndex && normalize(value) === normalized,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeValues(values: string[], fallback: string[]): string[] {
|
function normalizeValues(values: string[], fallback: string[]): string[] {
|
||||||
@ -58,8 +73,12 @@ export async function hydrateUiSettings(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const payload = await invoke<UiSettingsPayload>("get_ui_settings");
|
const payload = await invoke<UiSettingsPayload>("get_ui_settings");
|
||||||
settingsTags.set(normalizeValues(payload.tags ?? [], defaultTags));
|
settingsTags.set(normalizeValues(payload.tags ?? [], defaultTags));
|
||||||
settingsFragmentTypes.set(normalizeValues(payload.fragmentTypes ?? [], defaultFragmentTypes));
|
settingsFragmentTypes.set(
|
||||||
settingsDefaultStartupView.set(normalizeStartupView(payload.defaultStartupView));
|
normalizeValues(payload.fragmentTypes ?? [], defaultFragmentTypes),
|
||||||
|
);
|
||||||
|
settingsDefaultStartupView.set(
|
||||||
|
normalizeStartupView(payload.defaultStartupView),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[settings] hydrate failed", error);
|
console.error("[settings] hydrate failed", error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -70,7 +89,10 @@ export async function hydrateUiSettings(): Promise<void> {
|
|||||||
|
|
||||||
export async function persistUiSettings(): Promise<void> {
|
export async function persistUiSettings(): Promise<void> {
|
||||||
const tags = normalizeValues(get(settingsTags), defaultTags);
|
const tags = normalizeValues(get(settingsTags), defaultTags);
|
||||||
const fragmentTypes = normalizeValues(get(settingsFragmentTypes), defaultFragmentTypes);
|
const fragmentTypes = normalizeValues(
|
||||||
|
get(settingsFragmentTypes),
|
||||||
|
defaultFragmentTypes,
|
||||||
|
);
|
||||||
const startupView = normalizeStartupView(get(settingsDefaultStartupView));
|
const startupView = normalizeStartupView(get(settingsDefaultStartupView));
|
||||||
settingsTags.set(tags);
|
settingsTags.set(tags);
|
||||||
settingsFragmentTypes.set(fragmentTypes);
|
settingsFragmentTypes.set(fragmentTypes);
|
||||||
@ -81,7 +103,7 @@ export async function persistUiSettings(): Promise<void> {
|
|||||||
fragmentTypes,
|
fragmentTypes,
|
||||||
fragment_types: fragmentTypes,
|
fragment_types: fragmentTypes,
|
||||||
defaultStartupView: startupView,
|
defaultStartupView: startupView,
|
||||||
default_startup_view: startupView
|
default_startup_view: startupView,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +159,9 @@ export function updateFragmentType(index: number, value: string): boolean {
|
|||||||
const types = get(settingsFragmentTypes);
|
const types = get(settingsFragmentTypes);
|
||||||
if (index < 0 || index >= types.length) return false;
|
if (index < 0 || index >= types.length) return false;
|
||||||
if (hasDuplicate(types, next, index)) return false;
|
if (hasDuplicate(types, next, index)) return false;
|
||||||
settingsFragmentTypes.set(types.map((type, idx) => (idx === index ? next : type)));
|
settingsFragmentTypes.set(
|
||||||
|
types.map((type, idx) => (idx === index ? next : type)),
|
||||||
|
);
|
||||||
queuePersist();
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -157,4 +181,3 @@ export function setDefaultStartupView(value: string): boolean {
|
|||||||
queuePersist();
|
queuePersist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,17 @@ import {
|
|||||||
listTodoLists,
|
listTodoLists,
|
||||||
updateTodoItem as updateTodoItemCommand,
|
updateTodoItem as updateTodoItemCommand,
|
||||||
updateTodoList as updateTodoListCommand,
|
updateTodoList as updateTodoListCommand,
|
||||||
type TodoListDto
|
type TodoListDto,
|
||||||
} from "$lib/backend/todos";
|
} from "$lib/backend/todos";
|
||||||
|
|
||||||
// TodoItem keeps a numeric `id` for local array operations (used by EditorPanel)
|
// TodoItem keeps a numeric `id` for local array operations (used by EditorPanel)
|
||||||
// plus a `backendId` (guid string) for backend persistence.
|
// plus a `backendId` (guid string) for backend persistence.
|
||||||
export type TodoItem = { id: number; text: string; done: boolean; backendId?: string };
|
export type TodoItem = {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
done: boolean;
|
||||||
|
backendId?: string;
|
||||||
|
};
|
||||||
export type TodoListMeta = { id: string; label: string; backendId?: string };
|
export type TodoListMeta = { id: string; label: string; backendId?: string };
|
||||||
|
|
||||||
export const todoListsStore = writable<TodoListMeta[]>([]);
|
export const todoListsStore = writable<TodoListMeta[]>([]);
|
||||||
@ -42,7 +47,7 @@ function dtoToMeta(dto: TodoListDto): TodoListMeta {
|
|||||||
return {
|
return {
|
||||||
id: toStoreId(dto.id),
|
id: toStoreId(dto.id),
|
||||||
label: dto.label,
|
label: dto.label,
|
||||||
backendId: dto.id
|
backendId: dto.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +56,7 @@ function dtoToItems(dto: TodoListDto): TodoItem[] {
|
|||||||
id: createTodoId() + index,
|
id: createTodoId() + index,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
done: item.done,
|
done: item.done,
|
||||||
backendId: item.id
|
backendId: item.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +86,7 @@ export async function hydrateTodos(): Promise<void> {
|
|||||||
// ── List CRUD ────────────────────────────────────────────────────
|
// ── List CRUD ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function createTodoListFromLabel(
|
export async function createTodoListFromLabel(
|
||||||
label: string
|
label: string,
|
||||||
): Promise<{ meta: TodoListMeta; items: TodoItem[] }> {
|
): Promise<{ meta: TodoListMeta; items: TodoItem[] }> {
|
||||||
const resolvedLabel = label.trim() || "New List";
|
const resolvedLabel = label.trim() || "New List";
|
||||||
const created = await createTodoListCommand({ label: resolvedLabel });
|
const created = await createTodoListCommand({ label: resolvedLabel });
|
||||||
@ -92,7 +97,9 @@ export async function createTodoListFromLabel(
|
|||||||
return { meta, items: [] };
|
return { meta, items: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTodoListByStoreId(storeId: string): Promise<boolean> {
|
export async function deleteTodoListByStoreId(
|
||||||
|
storeId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
const backendId = toBackendId(storeId);
|
const backendId = toBackendId(storeId);
|
||||||
if (!backendId) return false;
|
if (!backendId) return false;
|
||||||
|
|
||||||
@ -111,7 +118,7 @@ export async function deleteTodoListByStoreId(storeId: string): Promise<boolean>
|
|||||||
|
|
||||||
export async function addTodoItemBackend(
|
export async function addTodoItemBackend(
|
||||||
storeId: string,
|
storeId: string,
|
||||||
text: string
|
text: string,
|
||||||
): Promise<TodoItem | null> {
|
): Promise<TodoItem | null> {
|
||||||
const backendListId = toBackendId(storeId);
|
const backendListId = toBackendId(storeId);
|
||||||
if (!backendListId || !text.trim()) return null;
|
if (!backendListId || !text.trim()) return null;
|
||||||
@ -122,26 +129,26 @@ export async function addTodoItemBackend(
|
|||||||
const created = await createTodoItemCommand({
|
const created = await createTodoItemCommand({
|
||||||
listId: backendListId,
|
listId: backendListId,
|
||||||
text: text.trim(),
|
text: text.trim(),
|
||||||
sortOrder
|
sortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const item: TodoItem = {
|
const item: TodoItem = {
|
||||||
id: createTodoId(),
|
id: createTodoId(),
|
||||||
text: created.text,
|
text: created.text,
|
||||||
done: created.done,
|
done: created.done,
|
||||||
backendId: created.id
|
backendId: created.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
todosStore.update((lists) => ({
|
todosStore.update((lists) => ({
|
||||||
...lists,
|
...lists,
|
||||||
[storeId]: [item, ...(lists[storeId] ?? [])]
|
[storeId]: [item, ...(lists[storeId] ?? [])],
|
||||||
}));
|
}));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleTodoItemBackend(
|
export async function toggleTodoItemBackend(
|
||||||
storeId: string,
|
storeId: string,
|
||||||
localId: number
|
localId: number,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const items = get(todosStore)[storeId];
|
const items = get(todosStore)[storeId];
|
||||||
const todo = items?.find((t) => t.id === localId);
|
const todo = items?.find((t) => t.id === localId);
|
||||||
@ -153,8 +160,8 @@ export async function toggleTodoItemBackend(
|
|||||||
todosStore.update((lists) => ({
|
todosStore.update((lists) => ({
|
||||||
...lists,
|
...lists,
|
||||||
[storeId]: (lists[storeId] ?? []).map((t) =>
|
[storeId]: (lists[storeId] ?? []).map((t) =>
|
||||||
t.id === localId ? { ...t, done: !t.done } : t
|
t.id === localId ? { ...t, done: !t.done } : t,
|
||||||
)
|
),
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -162,7 +169,7 @@ export async function toggleTodoItemBackend(
|
|||||||
export async function updateTodoItemTextBackend(
|
export async function updateTodoItemTextBackend(
|
||||||
storeId: string,
|
storeId: string,
|
||||||
localId: number,
|
localId: number,
|
||||||
text: string
|
text: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const items = get(todosStore)[storeId];
|
const items = get(todosStore)[storeId];
|
||||||
const todo = items?.find((t) => t.id === localId);
|
const todo = items?.find((t) => t.id === localId);
|
||||||
@ -174,15 +181,15 @@ export async function updateTodoItemTextBackend(
|
|||||||
todosStore.update((lists) => ({
|
todosStore.update((lists) => ({
|
||||||
...lists,
|
...lists,
|
||||||
[storeId]: (lists[storeId] ?? []).map((t) =>
|
[storeId]: (lists[storeId] ?? []).map((t) =>
|
||||||
t.id === localId ? { ...t, text: text.trim() } : t
|
t.id === localId ? { ...t, text: text.trim() } : t,
|
||||||
)
|
),
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeTodoItemBackend(
|
export async function removeTodoItemBackend(
|
||||||
storeId: string,
|
storeId: string,
|
||||||
localId: number
|
localId: number,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const items = get(todosStore)[storeId];
|
const items = get(todosStore)[storeId];
|
||||||
const todo = items?.find((t) => t.id === localId);
|
const todo = items?.find((t) => t.id === localId);
|
||||||
@ -193,7 +200,7 @@ export async function removeTodoItemBackend(
|
|||||||
|
|
||||||
todosStore.update((lists) => ({
|
todosStore.update((lists) => ({
|
||||||
...lists,
|
...lists,
|
||||||
[storeId]: (lists[storeId] ?? []).filter((t) => t.id !== localId)
|
[storeId]: (lists[storeId] ?? []).filter((t) => t.id !== localId),
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -202,7 +209,9 @@ export async function removeTodoItemBackend(
|
|||||||
|
|
||||||
export function serializeTodoList(title: string, todos: TodoItem[]): string {
|
export function serializeTodoList(title: string, todos: TodoItem[]): string {
|
||||||
const heading = title?.trim() ? `# ${title}` : "# To-Do List";
|
const heading = title?.trim() ? `# ${title}` : "# To-Do List";
|
||||||
const lines = todos.map((todo) => `- [${todo.done ? "x" : " "}] ${todo.text}`);
|
const lines = todos.map(
|
||||||
|
(todo) => `- [${todo.done ? "x" : " "}] ${todo.text}`,
|
||||||
|
);
|
||||||
return `${heading}\n\n${lines.join("\n")}`;
|
return `${heading}\n\n${lines.join("\n")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +224,7 @@ export function parseTodoList(content: string): TodoItem[] {
|
|||||||
parsed.push({
|
parsed.push({
|
||||||
id: createTodoId(),
|
id: createTodoId(),
|
||||||
text: match[2].trim(),
|
text: match[2].trim(),
|
||||||
done: match[1].toLowerCase() === "x"
|
done: match[1].toLowerCase() === "x",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
@ -224,7 +233,7 @@ export function parseTodoList(content: string): TodoItem[] {
|
|||||||
export function getOrCreateTodoList(
|
export function getOrCreateTodoList(
|
||||||
lists: Record<string, TodoItem[]>,
|
lists: Record<string, TodoItem[]>,
|
||||||
documentId: string,
|
documentId: string,
|
||||||
fallbackContent: string
|
fallbackContent: string,
|
||||||
): { lists: Record<string, TodoItem[]>; todos: TodoItem[] } {
|
): { lists: Record<string, TodoItem[]>; todos: TodoItem[] } {
|
||||||
const existing = lists[documentId];
|
const existing = lists[documentId];
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@ -237,7 +246,7 @@ export function getOrCreateTodoList(
|
|||||||
export function setTodoList(
|
export function setTodoList(
|
||||||
lists: Record<string, TodoItem[]>,
|
lists: Record<string, TodoItem[]>,
|
||||||
documentId: string,
|
documentId: string,
|
||||||
todos: TodoItem[]
|
todos: TodoItem[],
|
||||||
): Record<string, TodoItem[]> {
|
): Record<string, TodoItem[]> {
|
||||||
return { ...lists, [documentId]: todos };
|
return { ...lists, [documentId]: todos };
|
||||||
}
|
}
|
||||||
@ -247,21 +256,32 @@ export function addTodoItem(todos: TodoItem[], text: string): TodoItem[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleTodoItem(todos: TodoItem[], id: number): TodoItem[] {
|
export function toggleTodoItem(todos: TodoItem[], id: number): TodoItem[] {
|
||||||
return todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo));
|
return todos.map((todo) =>
|
||||||
|
todo.id === id ? { ...todo, done: !todo.done } : todo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTodoItemText(todos: TodoItem[], id: number, text: string): TodoItem[] {
|
export function updateTodoItemText(
|
||||||
return todos.map((todo) => (todo.id === id ? { ...todo, text: text.trim() } : todo));
|
todos: TodoItem[],
|
||||||
|
id: number,
|
||||||
|
text: string,
|
||||||
|
): TodoItem[] {
|
||||||
|
return todos.map((todo) =>
|
||||||
|
todo.id === id ? { ...todo, text: text.trim() } : todo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTodoItem(todos: TodoItem[], id: number): TodoItem[] {
|
export function removeTodoItem(todos: TodoItem[], id: number): TodoItem[] {
|
||||||
return todos.filter((todo) => todo.id !== id);
|
return todos.filter((todo) => todo.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTodoListDraft(): { meta: TodoListMeta; items: TodoItem[] } {
|
export function createTodoListDraft(): {
|
||||||
|
meta: TodoListMeta;
|
||||||
|
items: TodoItem[];
|
||||||
|
} {
|
||||||
const id = `todos/draft-${Date.now()}`;
|
const id = `todos/draft-${Date.now()}`;
|
||||||
return {
|
return {
|
||||||
meta: { id, label: "New List" },
|
meta: { id, label: "New List" },
|
||||||
items: []
|
items: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,11 @@ export function parseInline(input: string): string {
|
|||||||
value = value.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
value = value.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||||
value = value.replace(
|
value = value.replace(
|
||||||
/\[([^\]]+)\]\((journal:[^\s)]+)\)/g,
|
/\[([^\]]+)\]\((journal:[^\s)]+)\)/g,
|
||||||
'<a href="$2">$1</a>'
|
'<a href="$2">$1</a>',
|
||||||
);
|
);
|
||||||
value = value.replace(
|
value = value.replace(
|
||||||
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||||
'<a href="$2" target="_blank" rel="noreferrer">$1</a>'
|
'<a href="$2" target="_blank" rel="noreferrer">$1</a>',
|
||||||
);
|
);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -37,7 +37,9 @@ export function renderMarkdown(markdown: string): string {
|
|||||||
|
|
||||||
if (trimmed.startsWith("```")) {
|
if (trimmed.startsWith("```")) {
|
||||||
if (inCode) {
|
if (inCode) {
|
||||||
output.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
output.push(
|
||||||
|
`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`,
|
||||||
|
);
|
||||||
codeLines = [];
|
codeLines = [];
|
||||||
inCode = false;
|
inCode = false;
|
||||||
} else {
|
} else {
|
||||||
@ -69,7 +71,9 @@ export function renderMarkdown(markdown: string): string {
|
|||||||
if (/^[-*+]\s+/.test(trimmed)) {
|
if (/^[-*+]\s+/.test(trimmed)) {
|
||||||
const items: string[] = [];
|
const items: string[] = [];
|
||||||
while (i < lines.length && /^[-*+]\s+/.test(lines[i].trim())) {
|
while (i < lines.length && /^[-*+]\s+/.test(lines[i].trim())) {
|
||||||
items.push(`<li>${parseInline(lines[i].trim().replace(/^[-*+]\s+/, ""))}</li>`);
|
items.push(
|
||||||
|
`<li>${parseInline(lines[i].trim().replace(/^[-*+]\s+/, ""))}</li>`,
|
||||||
|
);
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
output.push(`<ul>${items.join("")}</ul>`);
|
output.push(`<ul>${items.join("")}</ul>`);
|
||||||
@ -79,7 +83,9 @@ export function renderMarkdown(markdown: string): string {
|
|||||||
if (/^\d+\.\s+/.test(trimmed)) {
|
if (/^\d+\.\s+/.test(trimmed)) {
|
||||||
const items: string[] = [];
|
const items: string[] = [];
|
||||||
while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
|
while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
|
||||||
items.push(`<li>${parseInline(lines[i].trim().replace(/^\d+\.\s+/, ""))}</li>`);
|
items.push(
|
||||||
|
`<li>${parseInline(lines[i].trim().replace(/^\d+\.\s+/, ""))}</li>`,
|
||||||
|
);
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
output.push(`<ol>${items.join("")}</ol>`);
|
output.push(`<ol>${items.join("")}</ol>`);
|
||||||
@ -87,7 +93,9 @@ export function renderMarkdown(markdown: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/^>\s+/.test(trimmed)) {
|
if (/^>\s+/.test(trimmed)) {
|
||||||
output.push(`<blockquote>${parseInline(trimmed.replace(/^>\s+/, ""))}</blockquote>`);
|
output.push(
|
||||||
|
`<blockquote>${parseInline(trimmed.replace(/^>\s+/, ""))}</blockquote>`,
|
||||||
|
);
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -114,7 +122,8 @@ export function renderMarkdown(markdown: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractEditorTitle(markdown: string, fallback: string): string {
|
export function extractEditorTitle(markdown: string, fallback: string): string {
|
||||||
const firstLine = markdown.replace(/\r\n/g, "\n").split("\n")[0]?.trim() ?? "";
|
const firstLine =
|
||||||
|
markdown.replace(/\r\n/g, "\n").split("\n")[0]?.trim() ?? "";
|
||||||
const headingMatch = firstLine.match(/^#\s+(.+)$/);
|
const headingMatch = firstLine.match(/^#\s+(.+)$/);
|
||||||
return headingMatch ? headingMatch[1] : fallback;
|
return headingMatch ? headingMatch[1] : fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,10 @@ function normalizeTags(tags: string[]): string[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitFrontmatter(content: string): { frontmatter: string | null; body: string } {
|
function splitFrontmatter(content: string): {
|
||||||
|
frontmatter: string | null;
|
||||||
|
body: string;
|
||||||
|
} {
|
||||||
const normalized = (content ?? "").replace(/\r\n/g, "\n");
|
const normalized = (content ?? "").replace(/\r\n/g, "\n");
|
||||||
if (!normalized.startsWith("---\n")) {
|
if (!normalized.startsWith("---\n")) {
|
||||||
return { frontmatter: null, body: normalized };
|
return { frontmatter: null, body: normalized };
|
||||||
@ -38,14 +41,12 @@ function parseTagsValue(rawValue: string): string[] {
|
|||||||
value
|
value
|
||||||
.slice(1, -1)
|
.slice(1, -1)
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((token) => token.trim().replace(/^["']|["']$/g, ""))
|
.map((token) => token.trim().replace(/^["']|["']$/g, "")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizeTags(
|
return normalizeTags(
|
||||||
value
|
value.split(",").map((token) => token.trim().replace(/^["']|["']$/g, "")),
|
||||||
.split(",")
|
|
||||||
.map((token) => token.trim().replace(/^["']|["']$/g, ""))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +60,9 @@ export function parseTagsFromMarkdown(content: string): string[] {
|
|||||||
const { frontmatter } = splitFrontmatter(content);
|
const { frontmatter } = splitFrontmatter(content);
|
||||||
if (!frontmatter) return [];
|
if (!frontmatter) return [];
|
||||||
|
|
||||||
const line = frontmatter.split("\n").find((entry) => /^\s*tags\s*:/i.test(entry));
|
const line = frontmatter
|
||||||
|
.split("\n")
|
||||||
|
.find((entry) => /^\s*tags\s*:/i.test(entry));
|
||||||
if (!line) return [];
|
if (!line) return [];
|
||||||
const value = line.replace(/^\s*tags\s*:/i, "");
|
const value = line.replace(/^\s*tags\s*:/i, "");
|
||||||
return parseTagsValue(value);
|
return parseTagsValue(value);
|
||||||
@ -72,7 +75,8 @@ export function stripFrontmatter(content: string): string {
|
|||||||
export function setTagsInMarkdown(content: string, tags: string[]): string {
|
export function setTagsInMarkdown(content: string, tags: string[]): string {
|
||||||
const normalizedBody = (content ?? "").replace(/\r\n/g, "\n");
|
const normalizedBody = (content ?? "").replace(/\r\n/g, "\n");
|
||||||
const normalizedTags = normalizeTags(tags);
|
const normalizedTags = normalizeTags(tags);
|
||||||
const tagsLine = normalizedTags.length > 0 ? `tags: ${formatTagsValue(normalizedTags)}` : "";
|
const tagsLine =
|
||||||
|
normalizedTags.length > 0 ? `tags: ${formatTagsValue(normalizedTags)}` : "";
|
||||||
const { frontmatter, body } = splitFrontmatter(normalizedBody);
|
const { frontmatter, body } = splitFrontmatter(normalizedBody);
|
||||||
|
|
||||||
if (!frontmatter) {
|
if (!frontmatter) {
|
||||||
@ -108,7 +112,7 @@ export function extractBracketTags(content: string): string[] {
|
|||||||
...raw
|
...raw
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((token) => token.trim())
|
.map((token) => token.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return normalizeTags(tokens);
|
return normalizeTags(tokens);
|
||||||
@ -137,7 +141,7 @@ export function extractTagsFromTagsSection(content: string): string[] {
|
|||||||
...cleaned
|
...cleaned
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((token) => token.trim())
|
.map((token) => token.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +152,6 @@ export function extractEntryTags(content: string): string[] {
|
|||||||
return normalizeTags([
|
return normalizeTags([
|
||||||
...parseTagsFromMarkdown(content),
|
...parseTagsFromMarkdown(content),
|
||||||
...extractBracketTags(content),
|
...extractBracketTags(content),
|
||||||
...extractTagsFromTagsSection(content)
|
...extractTagsFromTagsSection(content),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { getSessionPassword, clearVaultSession, flushBeforeClose } from "$lib/stores/session";
|
import {
|
||||||
|
getSessionPassword,
|
||||||
|
clearVaultSession,
|
||||||
|
flushBeforeClose,
|
||||||
|
} from "$lib/stores/session";
|
||||||
import { persistAndClearVault } from "$lib/backend/auth";
|
import { persistAndClearVault } from "$lib/backend/auth";
|
||||||
import { hydrateUiSettings } from "$lib/stores/settings";
|
import { hydrateUiSettings } from "$lib/stores/settings";
|
||||||
import { invoke, isTauriRuntime } from "$lib/runtime/invoke";
|
import { invoke, isTauriRuntime } from "$lib/runtime/invoke";
|
||||||
|
|||||||
@ -1,13 +1,40 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { unlockVaultWorkspace } from "$lib/backend/auth";
|
import { unlockVaultWorkspace } from "$lib/backend/auth";
|
||||||
import AppModal from "$lib/components/AppModal.svelte";
|
import AppModal from "$lib/components/AppModal.svelte";
|
||||||
import { deleteEntryTemplate, loadEntryTemplate, saveEntryTemplate } from "$lib/backend/templates";
|
import {
|
||||||
import { deleteEntryByStoreId, entriesStore, getDefaultEntry, hydrateEntries, loadEntryByStoreId, saveEntryFromStore } from "$lib/stores/entries";
|
deleteEntryTemplate,
|
||||||
import { deleteFragmentByStoreId, hydrateFragments } from "$lib/stores/fragments";
|
loadEntryTemplate,
|
||||||
import { deleteListByStoreId, hydrateLists, updateListByStoreId } from "$lib/stores/lists";
|
saveEntryTemplate,
|
||||||
import { hydrateUiSettings, settingsDefaultStartupView, type StartupView } from "$lib/stores/settings";
|
} from "$lib/backend/templates";
|
||||||
import { isVaultReady, setFlushCallback, setVaultSession } from "$lib/stores/session";
|
import {
|
||||||
|
deleteEntryByStoreId,
|
||||||
|
entriesStore,
|
||||||
|
getDefaultEntry,
|
||||||
|
hydrateEntries,
|
||||||
|
loadEntryByStoreId,
|
||||||
|
saveEntryFromStore,
|
||||||
|
} from "$lib/stores/entries";
|
||||||
|
import {
|
||||||
|
deleteFragmentByStoreId,
|
||||||
|
hydrateFragments,
|
||||||
|
} from "$lib/stores/fragments";
|
||||||
|
import {
|
||||||
|
deleteListByStoreId,
|
||||||
|
hydrateLists,
|
||||||
|
updateListByStoreId,
|
||||||
|
} from "$lib/stores/lists";
|
||||||
|
import {
|
||||||
|
hydrateUiSettings,
|
||||||
|
settingsDefaultStartupView,
|
||||||
|
type StartupView,
|
||||||
|
} from "$lib/stores/settings";
|
||||||
|
import {
|
||||||
|
isVaultReady,
|
||||||
|
setFlushCallback,
|
||||||
|
setVaultSession,
|
||||||
|
} from "$lib/stores/session";
|
||||||
import { deleteTodoListByStoreId, hydrateTodos } from "$lib/stores/todos";
|
import { deleteTodoListByStoreId, hydrateTodos } from "$lib/stores/todos";
|
||||||
import Navbar from "$lib/components/Navbar.svelte";
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
import SidePanel from "$lib/components/SidePanel.svelte";
|
import SidePanel from "$lib/components/SidePanel.svelte";
|
||||||
@ -41,7 +68,10 @@
|
|||||||
let activeDocumentLabel = initialEntry?.label ?? "Daily Notes";
|
let activeDocumentLabel = initialEntry?.label ?? "Daily Notes";
|
||||||
let openDocuments: Record<string, string> = initialEntry
|
let openDocuments: Record<string, string> = initialEntry
|
||||||
? { [initialEntry.id]: initialEntry.initialContent }
|
? { [initialEntry.id]: initialEntry.initialContent }
|
||||||
: { "entries/daily-notes": "# Daily Notes\n\nStart writing today's entry..." };
|
: {
|
||||||
|
"entries/daily-notes":
|
||||||
|
"# Daily Notes\n\nStart writing today's entry...",
|
||||||
|
};
|
||||||
let modalOpen = false;
|
let modalOpen = false;
|
||||||
let modalTitle = "";
|
let modalTitle = "";
|
||||||
let modalMessage = "";
|
let modalMessage = "";
|
||||||
@ -49,7 +79,12 @@
|
|||||||
let modalCancelText = "Cancel";
|
let modalCancelText = "Cancel";
|
||||||
let modalShowCancel = false;
|
let modalShowCancel = false;
|
||||||
let modalTone: "default" | "danger" = "default";
|
let modalTone: "default" | "danger" = "default";
|
||||||
let modalAction: "logout-confirm" | "logout-info" | "unlock-vault" | "delete-confirm" | null = null;
|
let modalAction:
|
||||||
|
| "logout-confirm"
|
||||||
|
| "logout-info"
|
||||||
|
| "unlock-vault"
|
||||||
|
| "delete-confirm"
|
||||||
|
| null = null;
|
||||||
let modalInputEnabled = false;
|
let modalInputEnabled = false;
|
||||||
let modalInputType = "text";
|
let modalInputType = "text";
|
||||||
let modalInputPlaceholder = "";
|
let modalInputPlaceholder = "";
|
||||||
@ -63,7 +98,7 @@
|
|||||||
let calendarPanelState: CalendarPanelState = {
|
let calendarPanelState: CalendarPanelState = {
|
||||||
items: [],
|
items: [],
|
||||||
busy: false,
|
busy: false,
|
||||||
error: ""
|
error: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveStartupSection(value: string): StartupView {
|
function resolveStartupSection(value: string): StartupView {
|
||||||
@ -130,7 +165,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showModal(options: {
|
function showModal(options: {
|
||||||
action: "logout-confirm" | "logout-info" | "unlock-vault" | "delete-confirm";
|
action:
|
||||||
|
| "logout-confirm"
|
||||||
|
| "logout-info"
|
||||||
|
| "unlock-vault"
|
||||||
|
| "delete-confirm";
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
@ -175,7 +214,7 @@
|
|||||||
action: "logout-info",
|
action: "logout-info",
|
||||||
title: "Logout Requested",
|
title: "Logout Requested",
|
||||||
message: "You have been logged out.",
|
message: "You have been logged out.",
|
||||||
confirmText: "Close"
|
confirmText: "Close",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -226,7 +265,7 @@
|
|||||||
inputType: "password",
|
inputType: "password",
|
||||||
inputPlaceholder: "Vault password",
|
inputPlaceholder: "Vault password",
|
||||||
inputAriaLabel: "Vault password",
|
inputAriaLabel: "Vault password",
|
||||||
inputValue: ""
|
inputValue: "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -234,7 +273,10 @@
|
|||||||
function isLockedError(error: unknown): boolean {
|
function isLockedError(error: unknown): boolean {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
const normalized = message.toLowerCase();
|
const normalized = message.toLowerCase();
|
||||||
return normalized.includes("database is locked") || normalized.includes("incorrect vault password");
|
return (
|
||||||
|
normalized.includes("database is locked") ||
|
||||||
|
normalized.includes("incorrect vault password")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrapFragmentsWithUnlock(maxAttempts = 3) {
|
async function bootstrapFragmentsWithUnlock(maxAttempts = 3) {
|
||||||
@ -284,7 +326,6 @@
|
|||||||
attempts += 1;
|
attempts += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
fragmentBootstrapInFlight = false;
|
fragmentBootstrapInFlight = false;
|
||||||
}
|
}
|
||||||
@ -296,39 +337,62 @@
|
|||||||
if (!content?.trim()) return;
|
if (!content?.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (selectedSection === "entries" && activeDocumentId.startsWith("entries/template-draft-")) {
|
if (
|
||||||
const draft = get(entriesStore).find((item) => item.id === activeDocumentId);
|
selectedSection === "entries" &&
|
||||||
|
activeDocumentId.startsWith("entries/template-draft-")
|
||||||
|
) {
|
||||||
|
const draft = get(entriesStore).find(
|
||||||
|
(item) => item.id === activeDocumentId,
|
||||||
|
);
|
||||||
const draftLabel = (draft?.label ?? activeDocumentLabel ?? "").trim();
|
const draftLabel = (draft?.label ?? activeDocumentLabel ?? "").trim();
|
||||||
const templateName = draftLabel.replace(/_template$/i, "").trim() || draftLabel;
|
const templateName =
|
||||||
|
draftLabel.replace(/_template$/i, "").trim() || draftLabel;
|
||||||
await saveEntryTemplate({
|
await saveEntryTemplate({
|
||||||
name: templateName,
|
name: templateName,
|
||||||
content
|
content,
|
||||||
});
|
});
|
||||||
templateRefreshToken += 1;
|
templateRefreshToken += 1;
|
||||||
entriesStore.update((items) => items.filter((item) => item.id !== activeDocumentId));
|
entriesStore.update((items) =>
|
||||||
|
items.filter((item) => item.id !== activeDocumentId),
|
||||||
|
);
|
||||||
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
||||||
openDocuments = rest;
|
openDocuments = rest;
|
||||||
activeDocumentId = "";
|
activeDocumentId = "";
|
||||||
activeDocumentLabel = "";
|
activeDocumentLabel = "";
|
||||||
editMode = false;
|
editMode = false;
|
||||||
} else if (selectedSection === "entries" && activeDocumentId.startsWith("entries/template-file/")) {
|
} else if (
|
||||||
|
selectedSection === "entries" &&
|
||||||
|
activeDocumentId.startsWith("entries/template-file/")
|
||||||
|
) {
|
||||||
const filePath = toTemplatePath(activeDocumentId);
|
const filePath = toTemplatePath(activeDocumentId);
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
await saveEntryTemplate({
|
await saveEntryTemplate({
|
||||||
name: templateNameFromPath(filePath),
|
name: templateNameFromPath(filePath),
|
||||||
content,
|
content,
|
||||||
filePath
|
filePath,
|
||||||
});
|
});
|
||||||
templateRefreshToken += 1;
|
templateRefreshToken += 1;
|
||||||
} else if (selectedSection === "entries" && (activeDocumentId.startsWith("entries/file/") || activeDocumentId.startsWith("entries/draft-"))) {
|
} else if (
|
||||||
const saved = await saveEntryFromStore(activeDocumentId, content, "Overwrite");
|
selectedSection === "entries" &&
|
||||||
|
(activeDocumentId.startsWith("entries/file/") ||
|
||||||
|
activeDocumentId.startsWith("entries/draft-"))
|
||||||
|
) {
|
||||||
|
const saved = await saveEntryFromStore(
|
||||||
|
activeDocumentId,
|
||||||
|
content,
|
||||||
|
"Overwrite",
|
||||||
|
);
|
||||||
if (saved && saved.id !== activeDocumentId) {
|
if (saved && saved.id !== activeDocumentId) {
|
||||||
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
||||||
openDocuments = { ...rest, [saved.id]: saved.initialContent };
|
openDocuments = { ...rest, [saved.id]: saved.initialContent };
|
||||||
activeDocumentId = saved.id;
|
activeDocumentId = saved.id;
|
||||||
activeDocumentLabel = saved.label;
|
activeDocumentLabel = saved.label;
|
||||||
}
|
}
|
||||||
} else if (selectedSection === "lists" && activeDocumentId.startsWith("lists/") && !activeDocumentId.startsWith("lists/draft-")) {
|
} else if (
|
||||||
|
selectedSection === "lists" &&
|
||||||
|
activeDocumentId.startsWith("lists/") &&
|
||||||
|
!activeDocumentId.startsWith("lists/draft-")
|
||||||
|
) {
|
||||||
await updateListByStoreId(activeDocumentId, undefined, content);
|
await updateListByStoreId(activeDocumentId, undefined, content);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -355,7 +419,7 @@
|
|||||||
confirmText: "Log Out",
|
confirmText: "Log Out",
|
||||||
cancelText: "Cancel",
|
cancelText: "Cancel",
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -393,7 +457,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let resolvedDoc = doc;
|
let resolvedDoc = doc;
|
||||||
if (effectiveSection === "entries" && doc.id.startsWith("entries/file/") && !doc.initialContent) {
|
if (
|
||||||
|
effectiveSection === "entries" &&
|
||||||
|
doc.id.startsWith("entries/file/") &&
|
||||||
|
!doc.initialContent
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const loaded = await loadEntryByStoreId(doc.id);
|
const loaded = await loadEntryByStoreId(doc.id);
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
@ -402,7 +470,11 @@
|
|||||||
} catch {
|
} catch {
|
||||||
// entry content will use initialContent fallback
|
// entry content will use initialContent fallback
|
||||||
}
|
}
|
||||||
} else if (effectiveSection === "entries" && doc.id.startsWith("entries/template-file/") && !doc.initialContent) {
|
} else if (
|
||||||
|
effectiveSection === "entries" &&
|
||||||
|
doc.id.startsWith("entries/template-file/") &&
|
||||||
|
!doc.initialContent
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const filePath = toTemplatePath(doc.id);
|
const filePath = toTemplatePath(doc.id);
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@ -410,7 +482,7 @@
|
|||||||
resolvedDoc = {
|
resolvedDoc = {
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
label: loaded.fileName.replace(/\.template\.md$/i, ""),
|
label: loaded.fileName.replace(/\.template\.md$/i, ""),
|
||||||
initialContent: loaded.content
|
initialContent: loaded.content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -419,7 +491,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(resolvedDoc.id in openDocuments)) {
|
if (!(resolvedDoc.id in openDocuments)) {
|
||||||
openDocuments = { ...openDocuments, [resolvedDoc.id]: resolvedDoc.initialContent };
|
openDocuments = {
|
||||||
|
...openDocuments,
|
||||||
|
[resolvedDoc.id]: resolvedDoc.initialContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
activeDocumentId = resolvedDoc.id;
|
activeDocumentId = resolvedDoc.id;
|
||||||
activeDocumentLabel = resolvedDoc.label;
|
activeDocumentLabel = resolvedDoc.label;
|
||||||
@ -433,7 +508,7 @@
|
|||||||
await handleOpenDocument({
|
await handleOpenDocument({
|
||||||
id: target.id,
|
id: target.id,
|
||||||
label: target.label,
|
label: target.label,
|
||||||
initialContent: target.initialContent
|
initialContent: target.initialContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +534,9 @@
|
|||||||
ok = await deleteEntryTemplate(templatePath);
|
ok = await deleteEntryTemplate(templatePath);
|
||||||
if (ok) templateRefreshToken += 1;
|
if (ok) templateRefreshToken += 1;
|
||||||
} else if (id.startsWith("entries/template-draft-")) {
|
} else if (id.startsWith("entries/template-draft-")) {
|
||||||
entriesStore.update((items) => items.filter((item) => item.id !== id));
|
entriesStore.update((items) =>
|
||||||
|
items.filter((item) => item.id !== id),
|
||||||
|
);
|
||||||
ok = true;
|
ok = true;
|
||||||
} else {
|
} else {
|
||||||
ok = await deleteEntryByStoreId(id);
|
ok = await deleteEntryByStoreId(id);
|
||||||
@ -500,7 +577,7 @@
|
|||||||
confirmText: "Delete",
|
confirmText: "Delete",
|
||||||
cancelText: "Cancel",
|
cancelText: "Cancel",
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,8 +585,12 @@
|
|||||||
setFlushCallback(saveCurrentDocument);
|
setFlushCallback(saveCurrentDocument);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
await hydrateUiSettings();
|
await hydrateUiSettings();
|
||||||
const startupSection = resolveStartupSection(get(settingsDefaultStartupView));
|
const startupSection = resolveStartupSection(
|
||||||
const sectionFromQuery = parseSectionQuery(new URLSearchParams(window.location.search).get("section"));
|
get(settingsDefaultStartupView),
|
||||||
|
);
|
||||||
|
const sectionFromQuery = parseSectionQuery(
|
||||||
|
new URLSearchParams(window.location.search).get("section"),
|
||||||
|
);
|
||||||
applyStartupSection(sectionFromQuery ?? startupSection);
|
applyStartupSection(sectionFromQuery ?? startupSection);
|
||||||
await bootstrapFragmentsWithUnlock();
|
await bootstrapFragmentsWithUnlock();
|
||||||
})();
|
})();
|
||||||
@ -569,6 +650,7 @@
|
|||||||
<style>
|
<style>
|
||||||
.app-shell {
|
.app-shell {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- @format -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import AppModal from "$lib/components/AppModal.svelte";
|
import AppModal from "$lib/components/AppModal.svelte";
|
||||||
@ -12,7 +13,7 @@
|
|||||||
settingsFragmentTypes,
|
settingsFragmentTypes,
|
||||||
settingsTags,
|
settingsTags,
|
||||||
updateFragmentType,
|
updateFragmentType,
|
||||||
updateSettingsTag
|
updateSettingsTag,
|
||||||
} from "$lib/stores/settings";
|
} from "$lib/stores/settings";
|
||||||
import { invoke, isTauriRuntime } from "$lib/runtime/invoke";
|
import { invoke, isTauriRuntime } from "$lib/runtime/invoke";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
@ -40,8 +41,16 @@
|
|||||||
let returnSection = "entries";
|
let returnSection = "entries";
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const queryReturn = new URLSearchParams(window.location.search).get("return")?.trim().toLowerCase() ?? "";
|
const queryReturn =
|
||||||
if (["entries", "calendar", "fragments", "todos", "lists"].includes(queryReturn)) {
|
new URLSearchParams(window.location.search)
|
||||||
|
.get("return")
|
||||||
|
?.trim()
|
||||||
|
.toLowerCase() ?? "";
|
||||||
|
if (
|
||||||
|
["entries", "calendar", "fragments", "todos", "lists"].includes(
|
||||||
|
queryReturn,
|
||||||
|
)
|
||||||
|
) {
|
||||||
returnSection = queryReturn;
|
returnSection = queryReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +66,9 @@
|
|||||||
async function saveSidecarRoot() {
|
async function saveSidecarRoot() {
|
||||||
sidecarRootError = "";
|
sidecarRootError = "";
|
||||||
try {
|
try {
|
||||||
const result: any = await invoke("set_sidecar_root", { path: sidecarRoot });
|
const result: any = await invoke("set_sidecar_root", {
|
||||||
|
path: sidecarRoot,
|
||||||
|
});
|
||||||
sidecarRoot = result.root;
|
sidecarRoot = result.root;
|
||||||
sidecarRootIsCustom = result.isCustom;
|
sidecarRootIsCustom = result.isCustom;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -88,7 +99,7 @@
|
|||||||
const picked = await open({
|
const picked = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
title: "Select Sidecar Root Directory"
|
title: "Select Sidecar Root Directory",
|
||||||
});
|
});
|
||||||
if (typeof picked === "string" && picked.trim()) {
|
if (typeof picked === "string" && picked.trim()) {
|
||||||
sidecarRoot = picked;
|
sidecarRoot = picked;
|
||||||
@ -131,7 +142,7 @@
|
|||||||
action: "logout-info",
|
action: "logout-info",
|
||||||
title: "Logout Requested",
|
title: "Logout Requested",
|
||||||
message: "You have been logged out.",
|
message: "You have been logged out.",
|
||||||
confirmText: "Close"
|
confirmText: "Close",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -148,7 +159,7 @@
|
|||||||
confirmText: "Log Out",
|
confirmText: "Log Out",
|
||||||
cancelText: "Cancel",
|
cancelText: "Cancel",
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -213,7 +224,9 @@
|
|||||||
|
|
||||||
function saveEditFragmentType() {
|
function saveEditFragmentType() {
|
||||||
if (editingFragmentTypeIndex === null) return;
|
if (editingFragmentTypeIndex === null) return;
|
||||||
if (updateFragmentType(editingFragmentTypeIndex, editingFragmentTypeValue)) {
|
if (
|
||||||
|
updateFragmentType(editingFragmentTypeIndex, editingFragmentTypeValue)
|
||||||
|
) {
|
||||||
editingFragmentTypeIndex = null;
|
editingFragmentTypeIndex = null;
|
||||||
editingFragmentTypeValue = "";
|
editingFragmentTypeValue = "";
|
||||||
}
|
}
|
||||||
@ -234,7 +247,6 @@
|
|||||||
function updateDefaultStartupView(value: string) {
|
function updateDefaultStartupView(value: string) {
|
||||||
setDefaultStartupView(value);
|
setDefaultStartupView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-shell panel-closed">
|
<div class="app-shell panel-closed">
|
||||||
@ -246,156 +258,225 @@
|
|||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<p>Configure app behavior and interface options.</p>
|
<p>Configure app behavior and interface options.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="header-close-btn" on:click={closeSettings} aria-label="Close settings">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="header-close-btn"
|
||||||
|
on:click={closeSettings}
|
||||||
|
aria-label="Close settings"
|
||||||
|
>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
<section class="route-card">
|
<section class="route-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
Startup
|
>rocket_launch</span
|
||||||
</h2>
|
>
|
||||||
</div>
|
Startup
|
||||||
<label>
|
</h2>
|
||||||
Default startup view
|
</div>
|
||||||
<select
|
<label>
|
||||||
value={$settingsDefaultStartupView}
|
Default startup view
|
||||||
on:change={(event) => updateDefaultStartupView((event.currentTarget as HTMLSelectElement).value)}
|
<select
|
||||||
>
|
value={$settingsDefaultStartupView}
|
||||||
<option value="entries">Entries</option>
|
on:change={(event) =>
|
||||||
<option value="calendar">Calendar</option>
|
updateDefaultStartupView(
|
||||||
<option value="fragments">Fragments</option>
|
(event.currentTarget as HTMLSelectElement).value,
|
||||||
<option value="todos">To-Do List</option>
|
)}
|
||||||
<option value="lists">Lists</option>
|
>
|
||||||
</select>
|
<option value="entries">Entries</option>
|
||||||
</label>
|
<option value="calendar">Calendar</option>
|
||||||
</section>
|
<option value="fragments">Fragments</option>
|
||||||
|
<option value="todos">To-Do List</option>
|
||||||
|
<option value="lists">Lists</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="route-card">
|
<section class="route-card list-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">label</span>
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
Tags
|
>label</span
|
||||||
</h2>
|
>
|
||||||
<p class="section-copy">Add and manage tags used for notes and entries.</p>
|
Tags
|
||||||
</div>
|
</h2>
|
||||||
|
<p class="section-copy">
|
||||||
|
Add and manage tags used for notes and entries.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="create-row">
|
<div class="create-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add tag (example: Research)"
|
placeholder="Add tag (example: Research)"
|
||||||
bind:value={newTag}
|
bind:value={newTag}
|
||||||
on:keydown={(event) => event.key === "Enter" && addTag()}
|
on:keydown={(event) => event.key === "Enter" && addTag()}
|
||||||
/>
|
/>
|
||||||
<button type="button" class="secondary-btn" on:click={addTag}>Add</button>
|
<button type="button" class="secondary-btn" on:click={addTag}
|
||||||
</div>
|
>Add</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="item-list">
|
<ul class="item-list">
|
||||||
{#each $settingsTags as tag, index}
|
{#each $settingsTags as tag, index}
|
||||||
<li class="item-row">
|
<li class="item-row">
|
||||||
{#if editingTagIndex === index}
|
{#if editingTagIndex === index}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={editingTagValue}
|
bind:value={editingTagValue}
|
||||||
on:keydown={(event) => {
|
on:keydown={(event) => {
|
||||||
if (event.key === "Enter") saveEditTag();
|
if (event.key === "Enter") saveEditTag();
|
||||||
if (event.key === "Escape") cancelEditTag();
|
if (event.key === "Escape") cancelEditTag();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<button type="button" class="secondary-btn" on:click={saveEditTag}>Save</button>
|
<button
|
||||||
<button type="button" class="ghost-btn" on:click={cancelEditTag}>Cancel</button>
|
type="button"
|
||||||
</div>
|
class="secondary-btn"
|
||||||
{:else}
|
on:click={saveEditTag}>Save</button
|
||||||
<span>{tag}</span>
|
>
|
||||||
<div class="row-actions">
|
<button
|
||||||
<button type="button" class="ghost-btn" on:click={() => startEditTag(index, tag)}>Edit</button>
|
type="button"
|
||||||
<button type="button" class="danger-btn" on:click={() => removeTag(index)}>Remove</button>
|
class="ghost-btn"
|
||||||
</div>
|
on:click={cancelEditTag}>Cancel</button
|
||||||
{/if}
|
>
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
{:else}
|
||||||
</ul>
|
<span>{tag}</span>
|
||||||
</section>
|
<div class="row-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ghost-btn"
|
||||||
|
on:click={() => startEditTag(index, tag)}>Edit</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="danger-btn"
|
||||||
|
on:click={() => removeTag(index)}>Remove</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="route-card">
|
<section class="route-card list-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">category</span>
|
<span class="material-symbols-outlined" aria-hidden="true"
|
||||||
Fragment Types
|
>category</span
|
||||||
</h2>
|
>
|
||||||
<p class="section-copy">Configure custom fragment types for the Fragments section.</p>
|
Fragment Types
|
||||||
</div>
|
</h2>
|
||||||
|
<p class="section-copy">
|
||||||
|
Configure custom fragment types for the Fragments section.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="create-row">
|
<div class="create-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add fragment type (example: Observation)"
|
placeholder="Add fragment type (example: Observation)"
|
||||||
bind:value={newFragmentType}
|
bind:value={newFragmentType}
|
||||||
on:keydown={(event) => event.key === "Enter" && addFragmentTypeLocal()}
|
on:keydown={(event) =>
|
||||||
/>
|
event.key === "Enter" && addFragmentTypeLocal()}
|
||||||
<button type="button" class="secondary-btn" on:click={addFragmentTypeLocal}>Add</button>
|
/>
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="secondary-btn"
|
||||||
|
on:click={addFragmentTypeLocal}>Add</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="item-list">
|
<ul class="item-list">
|
||||||
{#each $settingsFragmentTypes as type, index}
|
{#each $settingsFragmentTypes as type, index}
|
||||||
<li class="item-row">
|
<li class="item-row">
|
||||||
{#if editingFragmentTypeIndex === index}
|
{#if editingFragmentTypeIndex === index}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={editingFragmentTypeValue}
|
bind:value={editingFragmentTypeValue}
|
||||||
on:keydown={(event) => {
|
on:keydown={(event) => {
|
||||||
if (event.key === "Enter") saveEditFragmentType();
|
if (event.key === "Enter") saveEditFragmentType();
|
||||||
if (event.key === "Escape") cancelEditFragmentType();
|
if (event.key === "Escape") cancelEditFragmentType();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<button type="button" class="secondary-btn" on:click={saveEditFragmentType}>Save</button>
|
<button
|
||||||
<button type="button" class="ghost-btn" on:click={cancelEditFragmentType}>Cancel</button>
|
type="button"
|
||||||
</div>
|
class="secondary-btn"
|
||||||
{:else}
|
on:click={saveEditFragmentType}>Save</button
|
||||||
<span>{type}</span>
|
>
|
||||||
<div class="row-actions">
|
<button
|
||||||
<button type="button" class="ghost-btn" on:click={() => startEditFragmentType(index, type)}>Edit</button>
|
type="button"
|
||||||
<button type="button" class="danger-btn" on:click={() => removeFragmentTypeLocal(index)}>Remove</button>
|
class="ghost-btn"
|
||||||
</div>
|
on:click={cancelEditFragmentType}>Cancel</button
|
||||||
{/if}
|
>
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
{:else}
|
||||||
</ul>
|
<span>{type}</span>
|
||||||
</section>
|
<div class="row-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ghost-btn"
|
||||||
|
on:click={() => startEditFragmentType(index, type)}
|
||||||
|
>Edit</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="danger-btn"
|
||||||
|
on:click={() => removeFragmentTypeLocal(index)}
|
||||||
|
>Remove</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="route-card">
|
<section class="route-card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">hub</span>
|
<span class="material-symbols-outlined" aria-hidden="true">hub</span
|
||||||
Sidecar
|
>
|
||||||
</h2>
|
Sidecar
|
||||||
<p class="section-copy">Root directory containing the Journal.Sidecar project.</p>
|
</h2>
|
||||||
</div>
|
<p class="section-copy">
|
||||||
|
Root directory containing the Journal.Sidecar project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="create-row">
|
<div class="create-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Auto-detected from working directory"
|
placeholder="Auto-detected from working directory"
|
||||||
bind:value={sidecarRoot}
|
bind:value={sidecarRoot}
|
||||||
on:keydown={(event) => event.key === "Enter" && saveSidecarRoot()}
|
on:keydown={(event) => event.key === "Enter" && saveSidecarRoot()}
|
||||||
/>
|
/>
|
||||||
<button type="button" class="ghost-btn" on:click={browseSidecarRoot} disabled={sidecarBrowseBusy}>
|
<button
|
||||||
{sidecarBrowseBusy ? "Browsing..." : "Browse"}
|
type="button"
|
||||||
</button>
|
class="ghost-btn"
|
||||||
{#if sidecarRootIsCustom}
|
on:click={browseSidecarRoot}
|
||||||
<button type="button" class="ghost-btn" on:click={resetSidecarRoot}>Reset</button>
|
disabled={sidecarBrowseBusy}
|
||||||
|
>
|
||||||
|
{sidecarBrowseBusy ? "Browsing..." : "Browse"}
|
||||||
|
</button>
|
||||||
|
{#if sidecarRootIsCustom}
|
||||||
|
<button type="button" class="ghost-btn" on:click={resetSidecarRoot}
|
||||||
|
>Reset</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if sidecarRootError}
|
||||||
|
<p class="error-text">{sidecarRootError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{#if sidecarRootError}
|
|
||||||
<p class="error-text">{sidecarRootError}</p>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -414,11 +495,14 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.route-view {
|
.route-view {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
min-height: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
|
overflow: hidden;
|
||||||
background: var(--bg-editor);
|
background: var(--bg-editor);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
@ -459,7 +543,11 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
transition:
|
||||||
|
background-color 120ms ease,
|
||||||
|
color 120ms ease,
|
||||||
|
border-color 120ms ease,
|
||||||
|
transform 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-close-btn .material-symbols-outlined {
|
.header-close-btn .material-symbols-outlined {
|
||||||
@ -474,8 +562,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
columns: 2;
|
columns: 2;
|
||||||
column-gap: 14px;
|
column-gap: 14px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-card {
|
.route-card {
|
||||||
@ -487,12 +578,19 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 color-mix(in srgb, var(--zinc-300) 8%, transparent 92%),
|
inset 0 1px 0 color-mix(in srgb, var(--zinc-300) 8%, transparent 92%),
|
||||||
0 8px 24px color-mix(in srgb, var(--bg-app) 32%, transparent 68%);
|
0 8px 24px color-mix(in srgb, var(--bg-app) 32%, transparent 68%);
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: clamp(280px, 46vh, 520px);
|
||||||
|
}
|
||||||
|
|
||||||
.card-head {
|
.card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -527,7 +625,11 @@
|
|||||||
.route-card select {
|
.route-card select {
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: color-mix(in srgb, var(--surface-2) 88%, var(--bg-editor) 12%);
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--surface-2) 88%,
|
||||||
|
var(--bg-editor) 12%
|
||||||
|
);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 9px 34px 9px 10px;
|
padding: 9px 34px 9px 10px;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
@ -555,6 +657,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-row {
|
.item-row {
|
||||||
@ -598,7 +704,11 @@
|
|||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
transition:
|
||||||
|
background-color 120ms ease,
|
||||||
|
color 120ms ease,
|
||||||
|
border-color 120ms ease,
|
||||||
|
transform 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-btn {
|
.secondary-btn {
|
||||||
@ -633,6 +743,12 @@
|
|||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
columns: 1;
|
columns: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
max-height: min(42dvh, 460px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,16 +776,24 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
max-height: min(48dvh, 360px);
|
||||||
|
}
|
||||||
|
|
||||||
.item-row {
|
.item-row {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-row input,
|
||||||
|
.route-card input {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.row-actions {
|
.row-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: "Segoe UI Variable", "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
|
font-family:
|
||||||
|
"Segoe UI Variable", "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -51,7 +52,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: radial-gradient(circle at 15% -10%, var(--zinc-800) 0%, var(--bg-app) 42%);
|
background: radial-gradient(
|
||||||
|
circle at 15% -10%,
|
||||||
|
var(--zinc-800) 0%,
|
||||||
|
var(--bg-app) 42%
|
||||||
|
);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +89,7 @@ select {
|
|||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 72px 300px minmax(0, 1fr);
|
grid-template-columns: 72px 300px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user