Compare commits

...

51 Commits

Author SHA1 Message Date
Julien Calixte
1fa66d8594 fix: prevent spurious y-scrollbar when section has overflow-x: auto on mobile
Setting overflow-x: auto forces overflow-y off 'visible' per CSS spec,
which caused an unwanted vertical scrollbar in stacked note sections.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:28:25 +02:00
Julien Calixte
b827f31cf0 fix: current color for svg in buttons 2026-04-19 10:49:37 +02:00
Julien Calixte
cf02569c75 design: change light theme to emerald 2026-04-19 10:39:49 +02:00
Julien Calixte
0a4f8dbf41 fix: make BackButton and LinkedNotes keyboard accessible
Replace <a> (no href) with <button> so both elements receive tab focus.
BackButton gets text-base-content to preserve icon color; LinkedNotes
uses btn class="link" to keep the inline text-link appearance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 01:08:34 +02:00
Julien Calixte
b6f6759af5 fix: restore icon color on button elements in header
<button> gets color:ButtonText from the browser UA stylesheet, making
SVG stroke="currentColor" render black. Add text-base-content to
inherit the DaisyUI theme color like the <a>-based router-links do.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 01:07:25 +02:00
Julien Calixte
c42c26a407 fix: restore icon color on FontChange trigger button
<button> defaults to color: ButtonText (black) in browsers, unlike <a>
which inherits. Adding color: inherit restores the theme color for the
SVG stroke (which uses currentColor).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 01:06:25 +02:00
Julien Calixte
cfe5ef8fcd fix: make HomeButton keyboard accessible
Replace <a> with <button> so the home logo receives tab focus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 01:04:50 +02:00
Julien Calixte
4c5116bc89 fix: make FontChange modal trigger keyboard accessible
Replace <a> with <button> for the typography icon in HeaderNote so it
receives tab focus — <a> without href is excluded from the tab order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 01:02:12 +02:00
Julien Calixte
8581baafb7 design: change dark theme to forest 2026-04-08 19:07:03 +02:00
Julien Calixte
29c092e0a0 design: change dark theme to abyss 2026-04-08 19:04:28 +02:00
Julien Calixte
410c0cec7c design: change dark theme to sunset 2026-04-08 19:03:07 +02:00
Julien Calixte
66a1bcbaa9 design: change dark theme to black 2026-04-08 19:01:48 +02:00
Julien Calixte
541e058d12 fix: restore dark theme and fix theme script regex
Dark theme was set to "dim" in theme.config.ts while app.css registered
"sunset" as the prefersdark theme. The script's regex required a trailing
comma that didn't exist on the last property, causing silent failures.
2026-04-08 19:01:29 +02:00
Julien Calixte
a05ff9f238 design: change dark theme to sunset 2026-04-08 18:57:36 +02:00
Julien Calixte
6558de8df5 design: change dark theme to black 2026-04-08 18:51:30 +02:00
Julien Calixte
b48c1bd0d5 prune: remove obsolete agent 2026-04-06 23:41:19 +02:00
Julien Calixte
e369541dc0 refactor: scope PouchDB writes to repo config, not user font prefs
chosen* fields are per-browser preferences — localStorage is the correct
and sufficient store for them. Removing data.update from font setters and
stripping chosen* from the GitHub fetch PouchDB write prevents stale PouchDB
data from conflicting with localStorage on reload.
2026-04-06 23:26:50 +02:00
Julien Calixte
73a6014750 fix: persist font selections across navigation and page reloads
- Use v-model with writable computeds instead of :value+@change so selects
  re-sync when the options list changes asynchronously
- Always include currently chosen fonts in sortedFontFamilies so a selected
  font not present in .remanso.json fontFamilies still shows in the select
- Initialize userSettings instead of returning early in font setters so
  changes made before async GitHub fetch completes are not silently dropped
- Back font choices with localStorage so they survive hard reloads even when
  PouchDB/IndexedDB fails silently in the web worker
2026-04-06 18:51:27 +02:00
Julien Calixte
c197b80095 feat: smaller modal 2026-04-06 17:44:43 +02:00
Julien Calixte
f3e74aed34 fix: resolve all TypeScript type errors
- Install missing comlink (was in lockfile but not node_modules)
- Add @ts-rest/core and @ts-rest/vue-query (imported but not declared as deps)
- Add declare module '*.vue' shim to shims-vue.d.ts
- Replace arktype validators in ts-rest contract with contract.type<T>() since @ts-rest expects Zod schemas
2026-04-06 15:05:57 +02:00
Julien Calixte
8d9134a062 perf: cache repo list with 20-minute stale time
Hoist useRepos state to module scope so all callers share one instance, and skip re-fetching until data is older than 20 minutes.
2026-04-06 14:59:12 +02:00
Julien Calixte
006cd63388 feat: paginate repo list with infinite scroll
Load 30 repos at a time instead of 100 at once, showing data sooner.
Adds v-infinite-scroll to RepoList.vue to fetch subsequent pages on scroll.
2026-04-05 11:56:36 +02:00
Julien Calixte
3de9eb35f6 feat: show font family selectors with default fonts when no .remanso.json 2026-04-05 10:49:01 +02:00
Julien Calixte
99c349f6df fix: preserve font settings when repo has no .remanso.json
When no config file exists, userSettings was set to null which destroyed
cached user preferences and silently blocked all setFont* actions.
2026-04-04 14:39:34 +02:00
Julien Calixte
64b29bcdef fix: remove favicon.png from PWA manifest icons to fix dock icon on macOS 2026-04-04 14:22:14 +02:00
Julien Calixte
9e26e231cb fix: show theme and font size controls before font families load
Move the v-if guard from the outer FontChange wrapper to only the font-family
selects, so ThemeSwap and the font-size select are always visible in the modal
even before userSettings.fontFamilies resolves asynchronously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:35:05 +02:00
Julien Calixte
b003a3e008 perf: move PouchDB/IndexedDB operations to a Web Worker
All database reads and writes now run off the main thread via a
dedicated worker, eliminating IndexedDB overhead from the frame budget.

- Create data.worker.ts exposing the Data class via Comlink
- Refactor data.ts to export a Comlink-wrapped proxy and a standalone
  generateId() pure function (workers can't expose sync methods cleanly)
- Update all 10 call sites to import generateId directly instead of
  calling data.generateId()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:27:45 +02:00
Julien Calixte
1b5e23e3d4 fix: keep font settings visible during repo navigation
- resetFiles() no longer clears userSettings so FontChange stays visible
  while navigating between repos (old fonts show until new ones load)
- Add _requestId counter to setUserRepo() to discard stale async callbacks
  from previous navigations, preventing state corruption on quick nav
- Load savedRepo and userSettings caches in parallel with Promise.all,
  reducing yield points so cache hits apply before first render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:21:56 +02:00
Julien Calixte
52d7c84bd0 perf: prevent FPS drops during navigation in FluxNoteView
- Narrow backlinks watcher from entire store to store.files only,
  reducing trigger count from ~8 to 2 per navigation
- Defer computation start by 300ms so it runs after the 250ms view
  transition animation completes
- Yield to the browser between each file iteration using
  scheduler.yield() (with setTimeout fallback) to avoid blocking frames

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:14:02 +02:00
Julien Calixte
d76182b2c2 Merge branch 'main' of ssh://git.apoena.dev:22222/remanso-space/remanso 2026-04-03 15:02:46 +02:00
Julien Calixte
ed1a6b7fba fix: add the right margin to the right components 2026-03-29 22:09:01 +02:00
Julien Calixte
d5b251c4a0 fix: remove overflow because it's causing too much trouble 2026-03-29 22:00:22 +02:00
Julien Calixte
19b77810ec chore: remove healthcheck in docker to be faster 2026-03-29 21:55:32 +02:00
Julien Calixte
c8b0a78973 fix: add nginx SPA fallback to serve index.html for all routes
Prevents 404 errors when navigating directly to client-side routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:50:50 +02:00
Julien Calixte
087d1a355e revert: remove justify-content center from welcome content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:37:19 +01:00
Julien Calixte
5d90da8ab5 feat: center welcome content vertically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:36:40 +01:00
Julien Calixte
72d065975d fix: lock html/body to 100dvh overflow hidden on all screen sizes
All views that need scroll use their own overflow-y: auto containers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:35:11 +01:00
Julien Calixte
8b3df48791 fix: clip app at 100dvh to prevent body scroll on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:29:50 +01:00
Julien Calixte
cd8e173e05 fix: use 100dvh for body and #app to match dynamic viewport
Prevents white space below the app on Android Chrome where the
system nav bar makes 100vh > 100dvh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:25:19 +01:00
Julien Calixte
8767f7c430 fix: give .home explicit height so flex children resolve correctly
On Chrome Android, cross-axis stretch doesn't always produce a
definite height for inner flex items. Adding height: 100dvh to
.home ensures flex: 1 on .welcome-world resolves to full viewport.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:22:54 +01:00
Julien Calixte
369a200a42 fix: wrap content in flex:1 div so footer doesn't overflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:07:08 +01:00
Julien Calixte
06eaa3c9a7 fix: ensure footer stays at bottom with align-self stretch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:56:09 +01:00
Julien Calixte
4cbcf42e3d feat: replace BackButton and logo with HomeButton in PublicNoteView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:32:19 +01:00
Julien Calixte
a0be25c0dd fix: prevent layout shift on first load in PWA mode
Replace space-between with flex-start + margin-top:auto on footer and
add gap to avoid wide spacing while async components are loading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:28:47 +01:00
Julien Calixte
dcee26100f fix: use 100dvh to prevent scroll on mobile first load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:05:34 +01:00
Julien Calixte
ac68c68f8a feat: reorganize FontChange layout and resize header icons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 20:46:44 +01:00
Julien Calixte
982f3070a1 fix: use <a> for font modal trigger to match icon color
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 20:35:52 +01:00
Julien Calixte
20e9538983 feat: mv profile to footer 2026-03-28 20:24:08 +01:00
Julien Calixte
10c3e1ca60 feat: replace back button with HomeButton and fix view transition
- Use HomeButton component in HeaderNote for logo, hover, and view-transition-name
- Eagerly import HeaderNote in FluxNote so the logo exists in the DOM when the transition snapshot is taken
- Wait for afterEach + nextTick in the view transition hook to handle lazy-loaded routes
- Add cursor: pointer to font change button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 20:19:59 +01:00
6dc98c80ca Merge pull request 'chore/migrate-to-oxc' (#1) from chore/migrate-to-oxc into main
Reviewed-on: #1
2026-03-28 09:00:30 +00:00
Julien Calixte
1aef212a36 docs: rolldown and oxc 2026-03-22 01:23:13 +01:00
50 changed files with 812 additions and 768 deletions

View File

@@ -1,8 +0,0 @@
{
"name": "remanso-skills",
"version": "1.0.0",
"description": "Local skills for the Remanso project",
"author": {
"name": "julien"
}
}

View File

@@ -1,196 +0,0 @@
---
name: migrate-oxlint
description: Guide for migrating a project from ESLint to Oxlint. Use when asked to migrate, convert, or switch a JavaScript/TypeScript project's linter from ESLint to Oxlint.
---
This skill guides you through migrating a JavaScript/TypeScript project from ESLint to [Oxlint](https://oxc.rs/docs/guide/usage/linter/).
## Overview
Oxlint is a high-performance linter that implements many popular ESLint rules natively in Rust. It can be used alongside ESLint or as a full replacement.
An official migration tool is available, and will be used by this skill: [`@oxlint/migrate`](https://github.com/oxc-project/oxlint-migrate)
## Step 1: Run Automated Migration
Run the migration tool in the project root:
```bash
npx @oxlint/migrate
```
This reads your ESLint flat config (`eslint.config.js` for example) and generates a `.oxlintrc.json` file from it. It will find your ESLint config file automatically in most cases.
See options below for more info.
### Key Options
| Option | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `--type-aware` | Include type-aware rules from `@typescript-eslint` (will require the `oxlint-tsgolint` package to be installed after migrating) |
| `--with-nursery` | Include experimental rules still under development, may not be fully stable or consistent with ESLint equivalents |
| `--js-plugins [bool]` | Enable/disable ESLint plugin migration via `jsPlugins` (default: enabled) |
| `--details` | List rules that could not be migrated |
| `--replace-eslint-comments` | Convert all `// eslint-disable` comments to `// oxlint-disable` |
| `--output-file <file>` | Specify a different output path (default: `.oxlintrc.json`) |
If your ESLint config is not at the default location, pass the path explicitly:
```bash
npx @oxlint/migrate ./path/to/eslint.config.js
```
## Step 2: Review Generated Config
After migration, review the generated `.oxlintrc.json`.
### Plugin Mapping
The migration tool automatically maps ESLint plugins to oxlint's built-in equivalents. The following table is for reference when reviewing the generated config:
| ESLint Plugin | Oxlint Plugin Name |
| --------------------------------------------------- | ------------------ |
| `@typescript-eslint/eslint-plugin` | `typescript` |
| `eslint-plugin-react` / `eslint-plugin-react-hooks` | `react` |
| `eslint-plugin-import` / `eslint-plugin-import-x` | `import` |
| `eslint-plugin-unicorn` | `unicorn` |
| `eslint-plugin-jsx-a11y` | `jsx-a11y` |
| `eslint-plugin-react-perf` | `react-perf` |
| `eslint-plugin-promise` | `promise` |
| `eslint-plugin-jest` | `jest` |
| `@vitest/eslint-plugin` | `vitest` |
| `eslint-plugin-jsdoc` | `jsdoc` |
| `eslint-plugin-next` | `nextjs` |
| `eslint-plugin-node` | `node` |
| `eslint-plugin-vue` | `vue` |
Default plugins (enabled when `plugins` field is omitted): `unicorn`, `typescript`, `oxc`.
Setting the `plugins` array explicitly overrides these defaults.
ESLint core rules are usable in oxlint without needing to configure a plugin in the config file.
### Rule Categories
Oxlint groups rules into categories for bulk configuration, though only `correctness` is enabled by default:
```json
{
"categories": {
"correctness": "error",
"suspicious": "warn"
}
}
```
Available categories: `correctness` (default: enabled), `suspicious`, `pedantic`, `perf`, `style`, `restriction`, `nursery`.
Individual rule settings in `rules` override category settings.
`@oxlint/migrate` will turn `correctness` off to avoid enabling additional rules that weren't enabled by your ESLint config. You can choose to enable additional categories after migration if desired.
### Check Unmigrated Rules
Run with `--details` to see which ESLint rules could not be migrated:
```bash
npx @oxlint/migrate --details
```
Review the output and decide whether to keep ESLint for those rules or not. Some rules may be mentioned in the output from `--details` as having equivalents in oxlint that were not automatically mapped by the migration tool. In those cases, consider enabling the equivalent oxlint rule manually after migration.
## Step 3: Install Oxlint
Install the core oxlint package (use `yarn install`, `pnpm install`, `vp install`, `bun install`, etc. depending on your package manager):
```bash
npm install -D oxlint
```
If you want to add the `oxlint-tsgolint` package, if you intend to use type-aware rules that require TypeScript type information:
```bash
npm install -D oxlint-tsgolint
```
No other packages besides the above are needed by default, though you will need to keep/install any additional ESLint plugins that were migrated into `jsPlugins`. Do not add `@oxlint/migrate` to the package.json, it is meant for one-off usage.
## Step 4: Handle Unsupported Features
Some features require manual attention:
- Local plugins (relative path imports): Must be migrated manually to `jsPlugins`
- `eslint-plugin-prettier`: Supported, but very slow. It is recommended to use [oxfmt](https://oxc.rs/docs/guide/usage/formatter) instead, or switch to `prettier --check` as a separate step alongside oxlint.
- `settings` in override configs: Oxlint does not support `settings` inside `overrides` blocks.
- ESLint v9+ plugins: Not all work with oxlint's JS Plugins API, but the majority will.
### Local Plugins
If you have any custom ESLint rules in the project repo itself, you can migrate them manually after running the migration tool by adding them to the `jsPlugins` field in `.oxlintrc.json`:
```json
{
"jsPlugins": ["./path/to/my-plugin.js"],
"rules": {
"local-plugin/rule-name": "error"
}
}
```
### External ESLint Plugins
For ESLint plugins without a built-in oxlint equivalent, use the `jsPlugins` field to load them:
```json
{
"jsPlugins": ["eslint-plugin-custom"],
"rules": {
"custom/my-rule": "warn"
}
}
```
## Step 5: Update CI and Scripts
Replace ESLint commands with oxlint. Path arguments are optional; oxlint defaults to the current working directory.
```bash
# Before
npx eslint src/
npx eslint --fix src/
# After
npx oxlint src/
npx oxlint --fix src/
```
### Common CLI Options
| ESLint | oxlint equivalent |
| ------------------------- | ---------------------------------------------- |
| `eslint .` | `oxlint` (default: lints the cwd) |
| `eslint src/` | `oxlint src/` |
| `eslint --fix` | `oxlint --fix` |
| `eslint --max-warnings 0` | `oxlint --deny-warnings` or `--max-warnings 0` |
| `eslint --format json` | `oxlint --format json` |
Additional oxlint options:
- `--tsconfig <path>`: Specify tsconfig.json path, likely unnecessary unless you have a non-standard name for `tsconfig.json`.
## Tips
- You can run alongside ESLint if necessary: Oxlint is designed to complement ESLint during migration, but with JS Plugins many projects can switch over fully without losing many rules.
- Disable comments work: `// eslint-disable` and `// eslint-disable-next-line` comments are supported by oxlint. Use `--replace-eslint-comments` when running @oxlint/migrate to convert them to `// oxlint-disable` equivalents if desired.
- List available rules: Run `npx oxlint --rules` to see all supported rules, or refer to the [rule documentation](https://oxc.rs/docs/guide/usage/linter/rules.html).
- Schema support: Add `"$schema": "./node_modules/oxlint/configuration_schema.json"` to `.oxlintrc.json` for editor autocompletion if the migration tool didn't do it automatically.
- Output formats: `default`, `stylish`, `json`, `github`, `gitlab`, `junit`, `checkstyle`, `unix`
- Ignore files: `.eslintignore` is supported by oxlint if you have it, but it's recommended to move any ignore patterns into the `ignorePatterns` field in `.oxlintrc.json` for consistency and simplicity. All files and paths ignored via a `.gitignore` file will be ignored by oxlint by default as well.
- If you ran the migration tool multiple times, remove the `.oxlintrc.json.bak` backup file created by the migration tool once you've finished migrating.
- If you are not using any JS Plugins and have replaced your ESLint configuration, you can remove all ESLint packages from your project dependencies.
- Ensure your editor is configured to use oxlint instead of ESLint for linting and error reporting. You may want to install the Oxc extension for your preferred editor. See https://oxc.rs/docs/guide/usage/linter/editors.html for more details.
## References
- [CLI Reference](https://oxc.rs/docs/guide/usage/linter/cli.html)
- [Config File Reference](https://oxc.rs/docs/guide/usage/linter/config-file-reference.html)
- [Complete Oxlint rule list and docs](https://oxc.rs/docs/guide/usage/linter/rules.html)

View File

@@ -1,14 +0,0 @@
{
"name": "remanso-local",
"description": "Local plugins for the Remanso project",
"owner": {
"name": "julien"
},
"plugins": [
{
"name": "remanso-skills",
"description": "Local skills for the Remanso project (migrate-oxlint, etc.)",
"source": "./.agents"
}
]
}

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
node_modules
/dist
# local env files
.env.local
.env.*.local

View File

@@ -28,8 +28,6 @@ RUN pnpm run build
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:80/ || exit 1

View File

@@ -28,8 +28,8 @@ let themeConfigContent = readFileSync(themeConfigPath, "utf8")
// Remplacer la valeur du thème sombre
themeConfigContent = themeConfigContent.replace(
/dark:\s*['"][^'"]*['"],/,
`dark: '${newTheme}',`
/dark:\s*['"][^'"]*['"](,?)/,
`dark: '${newTheme}'$1`
)
// Écrire le contenu mis à jour dans le fichier

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-theme="garden">
<html lang="en" data-theme="emerald">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

9
nginx.conf Normal file
View File

@@ -0,0 +1,9 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -28,11 +28,14 @@
"@tailwindcss/postcss": "^4.1.16",
"@tanstack/vue-query": "^5.92.9",
"@toycode/markdown-it-class": "^1.2.4",
"@ts-rest/core": "^3.52.1",
"@ts-rest/vue-query": "^3.52.1",
"@vscode/markdown-it-katex": "^1.1.2",
"@vueuse/components": "^14.2.1",
"@vueuse/core": "^13.6.0",
"@vueuse/router": "^13.6.0",
"arktype": "^2.1.29",
"comlink": "^4.4.2",
"date-fns": "^4.1.0",
"events": "^3.3.0",
"font-color-contrast": "^11.1.0",

105
pnpm-lock.yaml generated
View File

@@ -38,6 +38,12 @@ importers:
'@toycode/markdown-it-class':
specifier: ^1.2.4
version: 1.2.4
'@ts-rest/core':
specifier: ^3.52.1
version: 3.52.1(@types/node@22.15.24)(zod@3.25.76)
'@ts-rest/vue-query':
specifier: ^3.52.1
version: 3.52.1(@tanstack/vue-query@5.92.9(vue@3.5.18(typescript@5.9.3)))(@ts-rest/core@3.52.1(@types/node@22.15.24)(zod@3.25.76))(zod@3.25.76)
'@vscode/markdown-it-katex':
specifier: ^1.1.2
version: 1.1.2
@@ -53,6 +59,9 @@ importers:
arktype:
specifier: ^2.1.29
version: 2.1.29
comlink:
specifier: ^4.4.2
version: 4.4.2
date-fns:
specifier: ^4.1.0
version: 4.1.0
@@ -1053,12 +1062,6 @@ packages:
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.4.0':
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1069,10 +1072,6 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint-community/regexpp@4.6.2':
resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/eslintrc@2.1.4':
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2119,6 +2118,27 @@ packages:
'@toycode/markdown-it-class@1.2.4':
resolution: {integrity: sha512-hA4gHBK8moObkOYdWTjhy1wYcYy0MJeM3JjSKbsXHRpRMvIKhk6Jm+t3bXsSScTdz/byWqQbs8YIwVYjHp+SlQ==}
'@ts-rest/core@3.52.1':
resolution: {integrity: sha512-tAjz7Kxq/grJodcTA1Anop4AVRDlD40fkksEV5Mmal88VoZeRKAG8oMHsDwdwPZz+B/zgnz0q2sF+cm5M7Bc7g==}
peerDependencies:
'@types/node': ^18.18.7 || >=20.8.4
zod: ^3.22.3
peerDependenciesMeta:
'@types/node':
optional: true
zod:
optional: true
'@ts-rest/vue-query@3.52.1':
resolution: {integrity: sha512-89u7aS9LGDC7uNUC5CagWX1EB7vTwyXohYcizLi1D9v7MD/Cnu5OTQNf8SY3PuAK62RcFJXB2XZGsMAPC0svNw==}
peerDependencies:
'@tanstack/vue-query': ^4.0.0
'@ts-rest/core': ~3.52.0
zod: ^3.22.3
peerDependenciesMeta:
zod:
optional: true
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -2985,6 +3005,9 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
comlink@4.4.2:
resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -3275,15 +3298,6 @@ packages:
supports-color:
optional: true
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -4959,9 +4973,6 @@ packages:
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -7642,25 +7653,17 @@ snapshots:
'@esbuild/win32-x64@0.25.5':
optional: true
'@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1':
optional: true
'@eslint-community/regexpp@4.6.2': {}
'@eslint-community/regexpp@4.12.1': {}
'@eslint/eslintrc@2.1.4':
dependencies:
ajv: 6.12.6
debug: 4.3.4
debug: 4.4.3
espree: 9.6.1
globals: 13.20.0
ignore: 5.2.4
@@ -7676,7 +7679,7 @@ snapshots:
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
debug: 4.3.4
debug: 4.4.3
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -8424,6 +8427,18 @@ snapshots:
'@toycode/markdown-it-class@1.2.4': {}
'@ts-rest/core@3.52.1(@types/node@22.15.24)(zod@3.25.76)':
optionalDependencies:
'@types/node': 22.15.24
zod: 3.25.76
'@ts-rest/vue-query@3.52.1(@tanstack/vue-query@5.92.9(vue@3.5.18(typescript@5.9.3)))(@ts-rest/core@3.52.1(@types/node@22.15.24)(zod@3.25.76))(zod@3.25.76)':
dependencies:
'@tanstack/vue-query': 5.92.9(vue@3.5.18(typescript@5.9.3))
'@ts-rest/core': 3.52.1(@types/node@22.15.24)(zod@3.25.76)
optionalDependencies:
zod: 3.25.76
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -8656,7 +8671,7 @@ snapshots:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.46.2
debug: 4.4.1
debug: 4.4.3
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
@@ -8676,7 +8691,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
'@typescript-eslint/types': 8.46.2
debug: 4.4.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -8707,7 +8722,7 @@ snapshots:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.1
debug: 4.4.3
eslint: 8.57.1
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
@@ -8742,7 +8757,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2
debug: 4.4.1
debug: 4.4.3
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@@ -9542,6 +9557,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
comlink@4.4.2: {}
commander@2.20.3: {}
commander@7.2.0: {}
@@ -9836,10 +9853,6 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.3.4:
dependencies:
ms: 2.1.2
debug@4.4.1:
dependencies:
ms: 2.1.3
@@ -10155,8 +10168,8 @@ snapshots:
eslint@8.57.1:
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
'@eslint-community/regexpp': 4.6.2
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
'@eslint-community/regexpp': 4.12.1
'@eslint/eslintrc': 2.1.4
'@eslint/js': 8.57.1
'@humanwhocodes/config-array': 0.13.0
@@ -10165,8 +10178,8 @@ snapshots:
'@ungap/structured-clone': 1.2.0
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.4
cross-spawn: 7.0.6
debug: 4.4.3
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@@ -11745,8 +11758,6 @@ snapshots:
ms@2.0.0: {}
ms@2.1.2: {}
ms@2.1.3: {}
multiformats@9.9.0: {}

View File

@@ -17,7 +17,7 @@ const { isATProtoReady } = useATProtoLogin()
<style lang="scss">
#main-app {
height: 100vh;
height: 100dvh;
width: 100%;
display: flex;
flex: 1;

View File

@@ -24,7 +24,7 @@ const goBack = () => {
</script>
<template>
<a class="btn btn-sm back-button" @click="goBack">
<button class="btn btn-sm back-button text-base-content" @click="goBack">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-narrow-left"
@@ -41,5 +41,5 @@ const goBack = () => {
<line x1="5" y1="12" x2="9" y2="16" />
<line x1="5" y1="12" x2="9" y2="8" />
</svg>
</a>
</button>
</template>

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup>
import {
computed,
defineAsyncComponent,
nextTick,
onMounted,
onUnmounted,
@@ -9,6 +8,7 @@ import {
watch
} from "vue"
import HeaderNote from "@/components/HeaderNote.vue"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
import StackedNote from "@/components/StackedNote.vue"
import { useLinks } from "@/hooks/useLinks.hook"
@@ -21,10 +21,6 @@ import CacheAllNotes from "@/modules/note/components/CacheAllNote.vue"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { useUserSettings } from "@/modules/user/hooks/useUserSettings.hook"
const HeaderNote = defineAsyncComponent(
() => import("@/components/HeaderNote.vue")
)
const props = withDefaults(
defineProps<{
user: string

View File

@@ -7,50 +7,106 @@ import { useUserRepoStore } from "../modules/repo/store/userRepo.store"
const store = useUserRepoStore()
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
const sortedFontFamilies = computed(() =>
[...fontFamilies.value].sort((a, b) => a.localeCompare(b))
const DEFAULT_FONT_FAMILIES = [
"EB Garamond",
"Inter",
"Lato",
"Libertinus Serif",
"Lora",
"Merriweather",
"Playfair Display",
"Roboto",
"Source Serif 4"
]
const fontFamilies = computed(
() => store.userSettings?.fontFamilies ?? DEFAULT_FONT_FAMILIES
)
const sortedFontFamilies = computed(() => {
const base = fontFamilies.value
const extras = [
store.userSettings?.chosenTitleFont,
store.userSettings?.chosenBodyFont
].filter((f): f is string => !!f && !base.includes(f))
return [...base, ...extras].sort((a, b) => a.localeCompare(b))
})
const fontSizes = Array.from({ length: 7 }, (_, i) => `${9 + i * 2}pt`)
const titleFont = computed({
get: () => store.userSettings?.chosenTitleFont,
set: (value) => store.setTitleFont(value!)
})
const bodyFont = computed({
get: () => store.userSettings?.chosenBodyFont,
set: (value) => store.setBodyFont(value!)
})
const fontSize = computed({
get: () => store.userSettings?.chosenFontSize,
set: (value) => store.setFontSize(value!)
})
</script>
<template>
<div class="font-change" v-if="sortedFontFamilies.length > 0">
<theme-swap />
<div class="font-change">
<div>
<label for="title-font" class="font-label">t</label>
<select
id="title-font"
class="select"
:value="store.userSettings?.chosenFontFamily"
@change="store.setFontFamily(($event.target as HTMLSelectElement).value)"
v-model="titleFont"
>
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
{{ font }}
</option>
</select>
<label for="body-font" class="font-label">p</label>
<select
id="body-font"
class="select"
:value="store.userSettings?.chosenFontSize"
@change="store.setFontSize(($event.target as HTMLSelectElement).value)"
v-model="bodyFont"
>
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
{{ font }}
</option>
</select>
</div>
<div>
<theme-swap />
<label for="font-size" class="font-label">s</label>
<select
id="font-size"
class="select"
v-model="fontSize"
>
<option v-for="size in fontSizes" :key="size" :value="size">
{{ size }}
</option>
</select>
</div>
</div>
</template>
<style lang="scss" scoped>
.font-change {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
select {
flex: 1;
display: flex;
}
div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
margin: 1rem;
}
}
.font-label {
font-weight: bold;
font-size: 0.75rem;
opacity: 0.6;
}
</style>

View File

@@ -1,32 +1,13 @@
<script lang="ts" setup>
import FontChange from "@/components/FontChange.vue"
import HomeButton from "@/components/HomeButton.vue"
defineProps<{ user: string; repo: string }>()
</script>
<template>
<header class="header-note">
<router-link
:to="{ name: 'Home' }"
class="button is-small is-white back-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-narrow-left"
width="28"
height="28"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="9" y2="16" />
<line x1="5" y1="12" x2="9" y2="8" />
</svg>
</router-link>
<home-button />
<!-- <router-link
:to="{ name: 'SpacedRepetitionCard', params: { user, repo } }"
>
@@ -51,12 +32,15 @@ defineProps<{ user: string; repo: string }>()
</svg>
</router-link> -->
<button onclick="font_modal.showModal()">
<button
class="btn btn-ghost btn-circle text-base-content"
onclick="font_modal.showModal()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icons-tabler-outline icon-tabler-typography"
width="36"
height="36"
width="30"
height="30"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -71,11 +55,14 @@ defineProps<{ user: string; repo: string }>()
<path d="M5 20l6 -16l2 0l7 16" />
</svg>
</button>
<router-link :to="{ name: 'FluxNoteView', params: { user, repo } }">
<router-link
class="btn btn-ghost btn-circle"
:to="{ name: 'FluxNoteView', params: { user, repo } }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
width="30"
height="30"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
@@ -88,12 +75,15 @@ defineProps<{ user: string; repo: string }>()
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
</svg>
</router-link>
<router-link :to="{ name: 'DraftNotes', params: { user, repo } }">
<router-link
class="btn btn-ghost btn-circle"
:to="{ name: 'DraftNotes', params: { user, repo } }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-notes"
width="36"
height="36"
width="30"
height="30"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -107,11 +97,14 @@ defineProps<{ user: string; repo: string }>()
<line x1="9" y1="15" x2="13" y2="15" />
</svg>
</router-link>
<router-link :to="{ name: 'TodoNotes', params: { user, repo } }">
<router-link
class="btn btn-ghost btn-circle"
:to="{ name: 'TodoNotes', params: { user, repo } }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
width="30"
height="30"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -129,12 +122,15 @@ defineProps<{ user: string; repo: string }>()
<path d="M11 18l9 0" />
</svg>
</router-link>
<router-link :to="{ name: 'FleetingNotes', params: { user, repo } }">
<router-link
class="btn btn-ghost btn-circle"
:to="{ name: 'FleetingNotes', params: { user, repo } }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-mailbox"
width="36"
height="36"
width="30"
height="30"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -150,7 +146,7 @@ defineProps<{ user: string; repo: string }>()
</svg>
</router-link>
<dialog id="font_modal" class="modal">
<div class="modal-box">
<div class="modal-box w-10/12 max-w-2xl">
<h3 class="text-lg font-bold">Style settings</h3>
<font-change />
</div>
@@ -168,12 +164,6 @@ defineProps<{ user: string; repo: string }>()
justify-content: space-between;
margin-top: 10px;
img {
&:hover {
cursor: pointer;
}
}
button {
color: var(--color-accent);
}

View File

@@ -6,9 +6,9 @@ const goHome = () => router.push({ name: "Home" })
</script>
<template>
<a class="btn btn-ghost btn-circle btn-lg" @click="goHome">
<button class="btn btn-ghost btn-circle btn-lg text-base-content" @click="goHome">
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
</a>
</button>
</template>
<style>

View File

@@ -24,9 +24,9 @@ const emitNote = (sha: string) => {
<h5 class="subtitle is-5">🔗</h5>
<ul class="links">
<li v-for="link in backlink?.links" :key="link.sha">
<a @click.prevent="emitNote(link.sha)">
<button class="link" @click="emitNote(link.sha)">
{{ link.title }}
</a>
</button>
</li>
</ul>
</div>

View File

@@ -40,6 +40,7 @@ const getStyle = (seed: string) => {
<style scoped lang="scss">
.repo-list {
display: flex;
justify-content: space-evenly;
gap: 1rem;
flex-wrap: wrap;

View File

@@ -278,6 +278,7 @@ $border-color: rgba(18, 19, 58, 0.2);
section {
padding: 1rem 0 2rem;
overflow-x: auto;
overflow-y: hidden;
}
.note-content {

View File

@@ -180,6 +180,7 @@ $border-color: rgba(18, 19, 58, 0.2);
section {
padding: 1rem 0 2rem;
overflow-x: auto;
overflow-y: hidden;
}
.note-content {

View File

@@ -3,16 +3,19 @@ import RepoList from "@/components/RepoList.vue"
import SignInAtproto from "@/components/SignInAtproto.vue"
import SignInGithub from "@/components/SignInGithub.vue"
import ThemeSwap from "@/components/ThemeSwap.vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
import { useForm } from "@/hooks/useForm.hook"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import LastVisited from "@/modules/history/components/LastVisited.vue"
const { isLogged } = useGitHubLogin()
const { isLoggedIn: isATProtoLoggedIn, avatarUrl } = useATProtoLogin()
const { userInput, repoInput, submit } = useForm()
</script>
<template>
<div class="welcome-world">
<div class="welcome-content">
<h1 class="title is-1">
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
Remanso
@@ -22,13 +25,6 @@ const { userInput, repoInput, submit } = useForm()
<last-visited />
<div class="get-started">
<sign-in-github />
<router-link v-if="isLogged" :to="{ name: 'RepoList' }" class="btn btn-sm"
>Manage your repos</router-link
>
</div>
<form class="github-form" @submit.prevent>
<div>github/</div>
<input
@@ -44,12 +40,15 @@ const { userInput, repoInput, submit } = useForm()
type="text"
placeholder="repo"
/>
<button type="submit" class="btn btn-primary" @click="submit">go</button>
<button type="submit" class="btn btn-sm btn-primary" @click="submit">
go
</button>
</form>
</div>
<footer>
<theme-swap />
Made with
made with
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-heart"
@@ -71,7 +70,32 @@ const { userInput, repoInput, submit } = useForm()
<a href="https://apoena.dev" target="_blank" rel="noopener noreferrer"
>apoena</a
>
<sign-in-atproto :with-sign-out="false" />
<button
class="btn btn-ghost btn-circle btn-sm profile-btn"
onclick="profile_modal.showModal()"
>
<img
v-if="isATProtoLoggedIn && avatarUrl"
:src="avatarUrl"
class="profile-avatar"
alt="Profile"
/>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
<path d="M6 20c0 -2.21 2.686 -4 6 -4s6 1.79 6 4" />
</svg>
</button>
<router-link
:to="{
name: 'FluxNoteView',
@@ -81,6 +105,28 @@ const { userInput, repoInput, submit } = useForm()
>Get started</router-link
>
</footer>
<dialog id="profile_modal" class="modal">
<div class="modal-box profile-modal-box">
<h3 class="text-lg font-bold">Profile</h3>
<div class="profile-section">
<sign-in-atproto :with-sign-out="true" />
</div>
<div class="divider"></div>
<div class="profile-section">
<sign-in-github />
<router-link
v-if="isLogged"
:to="{ name: 'RepoList' }"
class="btn btn-sm"
>Manage your repos</router-link
>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button></button>
</form>
</dialog>
</div>
</template>
@@ -98,26 +144,26 @@ h1 {
}
.welcome-world {
padding: 1rem;
margin: auto;
display: flex;
flex: 1;
align-self: stretch;
flex-direction: column;
justify-content: space-between;
.get-started {
margin: center;
text-align: center;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
.title {
text-align: center;
}
}
.welcome-content {
display: flex;
flex: 1;
flex-direction: column;
gap: 1rem;
padding: 0 0.5rem;
}
.github-form {
display: flex;
align-items: center;
@@ -127,15 +173,33 @@ h1 {
max-width: 140px;
}
}
footer {
display: flex;
gap: 1rem;
gap: 0.2rem;
align-items: center;
justify-content: space-around;
padding: 0.5rem;
}
img {
vertical-align: middle;
margin-top: 0;
}
.profile-avatar {
max-width: 100%;
border-radius: 50%;
object-fit: cover;
box-shadow: none;
}
.profile-modal-box {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.profile-section {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.to-user-repo {

View File

@@ -1,166 +1,31 @@
import { wrap } from "comlink"
import { nanoid } from "nanoid"
import indexedDb from "pouchdb-adapter-indexeddb"
import PouchDb from "pouchdb-browser"
import { DataType } from "./DataType.enum"
import { Model } from "./models/Model"
PouchDb.plugin(indexedDb)
interface GetAllParams {
export interface DataApi {
add<DT extends DataType>(model: Model<DT>): Promise<boolean>
update<DT extends DataType, T extends Model<DT>>(model: T): Promise<boolean>
remove(id: string): Promise<boolean>
get<DT extends DataType, T extends Model<DT>>(id: string): Promise<T | null>
getOrCreate<DT extends DataType, T extends Model<DT>>(
id: string,
initialValue: T
): Promise<T>
getAll<DT extends DataType, T extends Model<DT>>(params: {
prefix?: string
includeDocs?: boolean
includeAttachments?: boolean
keys?: string[]
}): Promise<T[]>
}
class Data {
// oxlint-disable-next-line typescript/ban-types
private readonly locale: PouchDB.Database<{}> | null = null
constructor() {
try {
this.locale = new PouchDb("remanso", {
adapter: "indexeddb"
})
} catch (error) {
console.warn("data error", error)
}
}
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
try {
const result = await this.locale?.put(model)
return result?.ok ?? false
} catch (error) {
console.warn(error)
return false
}
}
public async update<DT extends DataType, T extends Model<DT>>(
model: T
): Promise<boolean> {
try {
if (!model._id) {
const result = await this.locale?.put(model)
return result?.ok ?? false
}
const oldModel = await this.get(model._id)
if (oldModel) {
const result = await this.locale?.put({ ...oldModel, ...model })
return result?.ok ?? false
}
const result = await this.locale?.put(model)
return result?.ok ?? false
} catch (error) {
console.warn(error)
return false
}
}
public async remove(id: string): Promise<boolean> {
try {
const doc = await this.get(id)
if (!doc) {
return false
}
const result = await this.locale?.put({
...doc,
_deleted: true
})
return result?.ok ?? false
} catch {
return false
}
}
public async get<DT extends DataType, T extends Model<DT>>(
id: string
): Promise<T | null> {
try {
return ((await this.locale?.get(id)) as T) || null
} catch {
return null
}
}
public async getOrCreate<DT extends DataType, T extends Model<DT>>(
id: string,
initialValue: T
): Promise<T> {
const element = await this.get<DT, T>(id)
if (element) {
return element
}
await data.add<DT>({ ...initialValue, _id: id })
return this.getOrCreate(id, initialValue)
}
public async getAll<DT extends DataType, T extends Model<DT>>({
prefix,
includeDocs = true,
includeAttachments = false,
keys = []
}: GetAllParams): Promise<T[]> {
if (!this.locale) {
return []
}
if (keys.length) {
const response = await this.locale.allDocs({
include_docs: includeDocs,
attachments: includeAttachments,
keys: keys.map((key) => this.generateId(prefix, key))
})
if (includeDocs) {
return response.rows
.map((row) => {
if ("error" in row) {
return null
}
return row.doc
})
.filter(Boolean) as T[]
} else {
return response.rows
.map((row) => {
if ("error" in row) {
return null
}
return { _id: row.id }
})
.filter(Boolean) as T[]
}
}
const response = await this.locale.allDocs({
include_docs: includeDocs,
attachments: includeAttachments,
startkey: prefix ? prefix : undefined,
endkey: prefix ? `${prefix}\ufff0` : undefined
})
return response.rows.map((row) => row.doc) as T[]
}
public generateId(type?: DataType | string, id?: string) {
if (!type) {
return id || nanoid()
}
export const generateId = (type?: DataType | string, id?: string): string => {
if (!type) return id || nanoid()
return `${type}-${id || nanoid()}`
}
}
export const data = new Data()
import DataWorker from "./data.worker?worker"
export const data = wrap(new DataWorker()) as unknown as DataApi

156
src/data/data.worker.ts Normal file
View File

@@ -0,0 +1,156 @@
import { expose } from "comlink"
import { nanoid } from "nanoid"
import indexedDb from "pouchdb-adapter-indexeddb"
import PouchDb from "pouchdb-browser"
import { DataType } from "./DataType.enum"
import { Model } from "./models/Model"
PouchDb.plugin(indexedDb)
interface GetAllParams {
prefix?: string
includeDocs?: boolean
includeAttachments?: boolean
keys?: string[]
}
class Data {
// oxlint-disable-next-line typescript/ban-types
private readonly locale: PouchDB.Database<{}> | null = null
constructor() {
try {
this.locale = new PouchDb("remanso", {
adapter: "indexeddb"
})
} catch (error) {
console.warn("data error", error)
}
}
private buildId(type?: DataType | string, id?: string): string {
if (!type) return id || nanoid()
return `${type}-${id || nanoid()}`
}
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
try {
const result = await this.locale?.put(model)
return result?.ok ?? false
} catch (error) {
console.warn(error)
return false
}
}
public async update<DT extends DataType, T extends Model<DT>>(
model: T
): Promise<boolean> {
try {
if (!model._id) {
const result = await this.locale?.put(model)
return result?.ok ?? false
}
const oldModel = await this.get(model._id)
if (oldModel) {
const result = await this.locale?.put({ ...oldModel, ...model })
return result?.ok ?? false
}
const result = await this.locale?.put(model)
return result?.ok ?? false
} catch (error) {
console.warn(error)
return false
}
}
public async remove(id: string): Promise<boolean> {
try {
const doc = await this.get(id)
if (!doc) {
return false
}
const result = await this.locale?.put({
...doc,
_deleted: true
})
return result?.ok ?? false
} catch {
return false
}
}
public async get<DT extends DataType, T extends Model<DT>>(
id: string
): Promise<T | null> {
try {
return ((await this.locale?.get(id)) as T) || null
} catch {
return null
}
}
public async getOrCreate<DT extends DataType, T extends Model<DT>>(
id: string,
initialValue: T
): Promise<T> {
const element = await this.get<DT, T>(id)
if (element) {
return element
}
await this.add<DT>({ ...initialValue, _id: id })
return this.getOrCreate(id, initialValue)
}
public async getAll<DT extends DataType, T extends Model<DT>>({
prefix,
includeDocs = true,
includeAttachments = false,
keys = []
}: GetAllParams): Promise<T[]> {
if (!this.locale) {
return []
}
if (keys.length) {
const response = await this.locale.allDocs({
include_docs: includeDocs,
attachments: includeAttachments,
keys: keys.map((key) => this.buildId(prefix, key))
})
if (includeDocs) {
return response.rows
.map((row) => {
if ("error" in row) return null
return row.doc
})
.filter(Boolean) as T[]
} else {
return response.rows
.map((row) => {
if ("error" in row) return null
return { _id: row.id }
})
.filter(Boolean) as T[]
}
}
const response = await this.locale.allDocs({
include_docs: includeDocs,
attachments: includeAttachments,
startkey: prefix ? prefix : undefined,
endkey: prefix ? `${prefix}\ufff0` : undefined
})
return response.rows.map((row) => row.doc) as T[]
}
}
expose(new Data())

View File

@@ -14,14 +14,32 @@ import {
const did = ref<string | null>(null)
const handle = ref<string | null>(null)
const avatarUrl = ref<string | null>(null)
let init = true
const fetchAvatar = async (actorDid: string) => {
try {
const res = await fetch(
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actorDid)}`
)
if (res.ok) {
const data = await res.json()
avatarUrl.value = data.avatar ?? null
}
} catch {
avatarUrl.value = null
}
}
const initializeAuth = async () => {
// Load cached session from IndexedDB first (fast, local) so the UI can render immediately
const stored = await loadSession()
did.value = stored?.did ?? ""
handle.value = stored?.handle ?? ""
if (stored?.did) {
fetchAvatar(stored.did)
}
// Then restore OAuth session in the background (may involve network)
const session = await restoreSession()
@@ -32,6 +50,7 @@ const initializeAuth = async () => {
did.value = session.did
handle.value = resolvedHandle
await saveSession(session.did, resolvedHandle)
fetchAvatar(session.did)
window.history.replaceState(
null,
@@ -61,11 +80,13 @@ export const useATProtoLogin = () => {
await clearSession()
did.value = ""
handle.value = ""
avatarUrl.value = null
}
return {
did,
handle,
avatarUrl,
isLoggedIn,
isATProtoReady,
signIn,

View File

@@ -2,7 +2,7 @@ import { useAsyncState } from "@vueuse/core"
import { ComputedRef, onUnmounted, toValue } from "vue"
import { backlinkEventBus } from "@/bus/backlinkEventBus"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
@@ -11,7 +11,7 @@ export const useBacklinks = (sha: string | ComputedRef<string>) => {
const { state: backlink, execute } = useAsyncState(
data.get<DataType.BacklinkNote, BacklinkNote>(
data.generateId(DataType.BacklinkNote, sha)
generateId(DataType.BacklinkNote, sha)
),
null,
{

View File

@@ -1,7 +1,7 @@
import { watch } from "vue"
import { backlinkEventBus } from "@/bus/backlinkEventBus"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { useFile } from "@/hooks/useFile.hook"
import { Backlink } from "@/modules/note/models/Backlink"
@@ -14,10 +14,19 @@ import { confirmMessage } from "@/utils/notif"
const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
const yieldToMain = () =>
"scheduler" in globalThis
? (globalThis as unknown as { scheduler: { yield: () => Promise<void> } }).scheduler.yield()
: new Promise<void>((r) => setTimeout(r, 0))
export const useComputeBacklinks = () => {
const store = useUserRepoStore()
watch(store, async () => {
watch(
() => store.files,
async () => {
await new Promise<void>((r) => setTimeout(r, 300))
if (!store.userSettings?.backlink) {
return
}
@@ -27,11 +36,13 @@ export const useComputeBacklinks = () => {
const backlinks: Map<string, Backlink[]> = new Map()
for (const file of store.files) {
await yieldToMain()
if (!isMarkdown(file.path) || !file.sha) {
continue
}
const fileBacklinkId = data.generateId(DataType.BacklinkNote, file.sha)
const fileBacklinkId = generateId(DataType.BacklinkNote, file.sha)
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
fileBacklinkId
)
@@ -91,7 +102,7 @@ export const useComputeBacklinks = () => {
}
for (const [sha, fileBacklinks] of backlinks) {
const fileBacklinkId = data.generateId(DataType.BacklinkNote, sha)
const fileBacklinkId = generateId(DataType.BacklinkNote, sha)
const backlinkNote: BacklinkNote = {
_id: fileBacklinkId,
$type: DataType.BacklinkNote,
@@ -102,5 +113,6 @@ export const useComputeBacklinks = () => {
await data.update(backlinkNote)
backlinkEventBus.emit({ fileSha: sha })
}
})
}
)
}

View File

@@ -1,7 +1,7 @@
import { useAsyncState } from "@vueuse/core"
import { computed, ref } from "vue"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
import { Note } from "@/modules/note/models/Note"
@@ -36,7 +36,7 @@ export const useOfflineNotes = () => {
if (
!file.sha ||
cachedNotesSet.has(data.generateId(DataType.Note, file.sha))
cachedNotesSet.has(generateId(DataType.Note, file.sha))
) {
continue
}

View File

@@ -1,34 +1,59 @@
import { useAsyncState } from "@vueuse/core"
import { computed, ref } from "vue"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { getOctokit } from "@/modules/repo/services/octo"
const PER_PAGE = 30
const STALE_TIME_MS = 20 * 60 * 1000
const repos = ref<RepoBase[]>([])
const isReady = ref(false)
const currentPage = ref(0)
const totalCount = ref(0)
let lastFetchedAt = 0
export const useRepos = () => {
const { username, accessToken } = useGitHubLogin()
const repos = useAsyncState<RepoBase[]>(async () => {
const loadMore = async () => {
if (!accessToken.value || !username.value) {
return []
isReady.value = true
return
}
const octokit = await getOctokit()
const nextPage = currentPage.value + 1
const repoList = await octokit.request("GET /search/repositories", {
q: `user:${username.value}`,
per_page: 100
per_page: PER_PAGE,
page: nextPage
})
return repoList.data.items
.map((item) => ({
currentPage.value = nextPage
totalCount.value = repoList.data.total_count
const newItems = repoList.data.items.map((item) => ({
id: `${item.id}`,
name: item.name,
isPrivate: item.private
}))
.sort((a, b) => (a.name < b.name ? -1 : 1))
}, [])
return {
repos: repos.state,
isReady: repos.isReady
repos.value = [...repos.value, ...newItems].sort((a, b) =>
a.name < b.name ? -1 : 1
)
isReady.value = true
}
const canLoadMore = computed(() => repos.value.length < totalCount.value)
const isStale = Date.now() - lastFetchedAt > STALE_TIME_MS
if (!isReady.value || isStale) {
if (isStale && isReady.value) {
repos.value = []
currentPage.value = 0
totalCount.value = 0
isReady.value = false
}
lastFetchedAt = Date.now()
loadMore()
}
return { repos, isReady, canLoadMore, loadMore }
}

View File

@@ -4,7 +4,7 @@ import { useAsyncState } from "@vueuse/core"
import { addDays, isAfter } from "date-fns"
import { computed, nextTick, watch } from "vue"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { useFile } from "@/hooks/useFile.hook"
import { useLinks } from "@/hooks/useLinks.hook"
@@ -51,7 +51,7 @@ export const useSpacedRepetitionCards = () => {
const repetition = await data.getOrCreate<
DataType.RepetitionCard,
RepetitionCard
>(data.generateId(DataType.RepetitionCard, cardFile.path), {
>(generateId(DataType.RepetitionCard, cardFile.path), {
$type: DataType.RepetitionCard,
level: 1,
repeatDate: new Date(),

View File

@@ -1,17 +1,17 @@
import { useAsyncState } from "@vueuse/core"
import { computed } from "vue"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { History } from "@/data/models/History"
const HISTORY_ID = data.generateId(DataType.History, "history")
const HISTORY_ID = generateId(DataType.History, "history")
export const useLastVisitedRepos = () => {
const history = useAsyncState(
() =>
data.get<DataType.History, History>(
data.generateId(DataType.History, "history")
generateId(DataType.History, "history")
),
null
)

View File

@@ -1,10 +1,10 @@
import { Ref, toValue } from "vue"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { History } from "@/data/models/History"
const HISTORY_ID = data.generateId(DataType.History, "history")
const HISTORY_ID = generateId(DataType.History, "history")
const MAX_REPO_HISTORY = 10
export const useVisitRepo = (newRepo: {

View File

@@ -1,4 +1,4 @@
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { Note } from "@/modules/note/models/Note"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
@@ -14,8 +14,8 @@ type NoteCacheResult =
export const prepareNoteCache = (sha: string, path?: string) => {
const store = useUserRepoStore()
const noteId = data.generateId(DataType.Note, sha)
const notePath = path ? data.generateId(DataType.Note, path) : null
const noteId = generateId(DataType.Note, sha)
const notePath = path ? generateId(DataType.Note, path) : null
const getCachedNote = async (): Promise<NoteCacheResult> => {
const note = await data.get<DataType.Note, Note>(noteId)

View File

@@ -29,31 +29,19 @@ export const noteRouter = contract.router({
noteLists: {
method: "GET",
path: "/notes",
query: type({
cursor: "string | undefined",
limit: "number | undefined"
}),
query: contract.type<{ cursor?: string; limit?: number }>(),
responses: {
200: type({
notes: PublicNoteListItem.array()
})
200: contract.type<{ notes: PublicNoteListItem[] }>()
},
summary: "List all notes"
},
noteListsByDid: {
method: "GET",
path: "/:did/notes",
pathParams: type({
did: "string"
}),
query: type({
cursor: "string | undefined",
limit: "number | undefined"
}),
pathParams: contract.type<{ did: string }>(),
query: contract.type<{ cursor?: string; limit?: number }>(),
responses: {
200: type({
notes: PublicNoteListItem.array()
})
200: contract.type<{ notes: PublicNoteListItem[] }>()
},
summary: "List all notes"
}

View File

@@ -1,6 +1,6 @@
import { computed, onMounted, ref } from "vue"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { useRepos } from "@/hooks/useRepos.hook"
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
@@ -27,7 +27,7 @@ export const useFavoriteRepos = () => {
const toggleFavorite = async (repo: RepoBase, isFavorite: boolean) => {
const favorite: FavoriteRepo = {
_id: data.generateId(DataType.FavoriteRepo, repo.id),
_id: generateId(DataType.FavoriteRepo, repo.id),
$type: DataType.FavoriteRepo,
isFavorite,
name: repo.name,

View File

@@ -6,7 +6,7 @@ import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
export const useRepoList = () => {
const { savedFavoriteRepos, addFavorite, removeFavorite } = useFavoriteRepos()
const { repos } = useRepos()
const { repos, canLoadMore, loadMore } = useRepos()
const favoriteRepos = computed(() => {
return repos.value.filter((repo) =>
@@ -38,6 +38,8 @@ export const useRepoList = () => {
favoriteRepos,
otherRepos,
favoriteCheckboxes,
toggleCheckbox
toggleCheckbox,
canLoadMore,
loadMore
}
}

View File

@@ -8,4 +8,6 @@ export interface UserSettings extends Model<DataType.UserSettings> {
fontSize?: string
chosenFontSize?: string
backlink?: boolean
chosenTitleFont?: string
chosenBodyFont?: string
}

View File

@@ -104,7 +104,16 @@ export const getUserSettingsContent = async (
return null
}
return JSON.parse(atob(content)) as UserSettings
const raw = JSON.parse(atob(content)) as UserSettings & {
t?: string
p?: string
}
const { t, p, ...rest } = raw
return {
...rest,
chosenTitleFont: t,
chosenBodyFont: p
}
}
export const queryFileContent = async (

View File

@@ -1,6 +1,6 @@
import { defineStore } from "pinia"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
@@ -20,6 +20,7 @@ interface State {
readme?: string | null
userSettings?: UserSettings | null
needToLogin: boolean
_requestId: number
}
export const useUserRepoStore = defineStore("USER_REPO_STATE", {
@@ -29,40 +30,71 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
files: [],
readme: undefined,
userSettings: undefined,
needToLogin: false
needToLogin: false,
_requestId: 0
}),
actions: {
_persistFonts() {
if (!this.userSettings) return
try {
const { chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily } =
this.userSettings
localStorage.setItem(
`remanso:fonts:${this.user}:${this.repo}`,
JSON.stringify({ chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily })
)
} catch {
// ignore
}
},
async setUserRepo(user: string, repo: string) {
const requestId = ++this._requestId
this.user = user
this.repo = repo
const savedRepoId = data.generateId(DataType.SavedRepo, `${user}-${repo}`)
const cachedSavedRepo = await data.get<DataType.SavedRepo, SavedRepo>(
savedRepoId
)
let lsFonts: Partial<UserSettings> = {}
try {
const lsRaw = localStorage.getItem(`remanso:fonts:${user}:${repo}`)
if (lsRaw) lsFonts = JSON.parse(lsRaw)
} catch {
// ignore
}
if (Object.keys(lsFonts).length) {
if (!this.userSettings) this.userSettings = { $type: DataType.UserSettings }
Object.assign(this.userSettings, lsFonts)
}
const savedRepoId = generateId(DataType.SavedRepo, `${user}-${repo}`)
const userSettingsId = `UserSetting-${user}-${repo}`
const [cachedSavedRepo, cachedUserSettings] = await Promise.all([
data.get<DataType.SavedRepo, SavedRepo>(savedRepoId),
data.get<DataType.UserSettings, UserSettings>(userSettingsId)
])
if (requestId !== this._requestId) return
if (cachedSavedRepo) {
this.files = cachedSavedRepo.files
}
if (cachedUserSettings) {
// localStorage font choices take priority over PouchDB cache
this.userSettings = { ...cachedUserSettings, ...lsFonts }
}
try {
await refreshToken()
} catch (error) {
console.warn("impossible to refresh token", error)
}
const userSettingsId = `UserSetting-${user}-${repo}`
const cachedUserSettings = await data.get<
DataType.UserSettings,
UserSettings
>(userSettingsId)
if (cachedUserSettings) {
this.userSettings = cachedUserSettings
}
if (requestId !== this._requestId) return
getFiles(user, repo)
.then(async (files) => {
if (requestId !== this._requestId) return
data.update<DataType.SavedRepo, SavedRepo>({
_id: savedRepoId,
$type: DataType.SavedRepo,
@@ -74,6 +106,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
return getUserSettingsContent(user, repo, files)
})
.then((userSettings) => {
if (requestId !== this._requestId) return
const chosenFontFamily = userSettings?.fontFamilies?.find(
(font) => font === this.userSettings?.chosenFontFamily
)
@@ -81,24 +114,43 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
: userSettings?.fontFamily
const chosenFontSize =
this.userSettings?.chosenFontSize ?? userSettings?.fontSize
const chosenTitleFont =
this.userSettings?.chosenTitleFont ??
userSettings?.chosenTitleFont ??
chosenFontFamily
const chosenBodyFont =
this.userSettings?.chosenBodyFont ??
userSettings?.chosenBodyFont ??
chosenFontFamily
if (userSettings) {
this.userSettings = userSettings
if (!this.userSettings) {
return
} else if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenFontFamily =
chosenFontFamily ?? this.userSettings.fontFamily
this.userSettings.chosenFontSize =
chosenFontSize ?? this.userSettings.fontSize
this.userSettings.chosenTitleFont = chosenTitleFont
this.userSettings.chosenBodyFont = chosenBodyFont
// Persist only repo config fields — chosen* are localStorage-only
const {
chosenTitleFont: _t,
chosenBodyFont: _b,
chosenFontSize: _s,
chosenFontFamily: _f,
...repoConfig
} = this.userSettings
data.update<DataType.UserSettings, UserSettings>({
...this.userSettings,
...repoConfig,
_id: userSettingsId
})
})
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
if (requestId !== this._requestId) return
this.readme = cachedReadme
this.readme = await getMainReadme(user, repo)
})
@@ -114,7 +166,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
return
}
const savedRepoId = data.generateId(
const savedRepoId = generateId(
DataType.SavedRepo,
`${this.user}-${this.repo}`
)
@@ -132,35 +184,39 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
this.user = ""
this.repo = ""
this.resetFiles()
this.userSettings = undefined
},
resetFiles() {
this.files = []
this.readme = null
this.userSettings = undefined
},
setFontFamily(fontFamily: string) {
if (!this.userSettings) {
return
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenFontFamily = fontFamily
const userSettingsId = `UserSetting-${this.user}-${this.repo}`
data.update<DataType.UserSettings, UserSettings>({
...this.userSettings,
_id: userSettingsId
})
this._persistFonts()
},
setFontSize(fontSize: string) {
if (!this.userSettings) {
return
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenFontSize = fontSize
const userSettingsId = `UserSetting-${this.user}-${this.repo}`
data.update<DataType.UserSettings, UserSettings>({
...this.userSettings,
_id: userSettingsId
})
this._persistFonts()
},
setTitleFont(font: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenTitleFont = font
this._persistFonts()
},
setBodyFont(font: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenBodyFont = font
this._persistFonts()
}
}
})

View File

@@ -12,10 +12,15 @@ export const useUserSettings = () => {
watchEffect(() => {
const root = document.documentElement
const fontFamily = store.userSettings?.chosenFontFamily
const fontSize = store.userSettings?.chosenFontSize
const bodyFont = store.userSettings?.chosenBodyFont
const titleFont = store.userSettings?.chosenTitleFont
downloadFont(fontFamily || DEFAULT_FONT_POLICY)
downloadFont(bodyFont || DEFAULT_FONT_POLICY, "--font-family")
downloadFont(
titleFont || bodyFont || DEFAULT_FONT_POLICY,
"--title-font-family"
)
root.style.setProperty("--font-size", fontSize || DEFAULT_FONT_SIZE)
})
}

View File

@@ -1,7 +1,7 @@
import { Octokit } from "@octokit/rest"
import { addMinutes, addSeconds, isBefore } from "date-fns"
import { data } from "@/data/data"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { GithubAccessToken } from "@/data/models/GithubAccessToken"
import { GithubToken } from "@/modules/user/interfaces/GithubToken"
@@ -26,7 +26,7 @@ export const needToRefreshToken = async () => {
const accessToken = await data.get<
DataType.GithubAccessToken,
GithubAccessToken
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
>(generateId(DataType.GithubAccessToken, personalTokenId))
if (!accessToken) {
return false
@@ -42,7 +42,7 @@ export const refreshToken = async () => {
const accessToken = await data.get<
DataType.GithubAccessToken,
GithubAccessToken
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
>(generateId(DataType.GithubAccessToken, personalTokenId))
if (!accessToken) {
return null
@@ -74,7 +74,7 @@ export const getAccessToken = async () => {
const response = await data.get<
DataType.GithubAccessToken,
GithubAccessToken
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
>(generateId(DataType.GithubAccessToken, personalTokenId))
return response
}
@@ -94,7 +94,7 @@ export const saveAccessToken = async (githubToken: GithubToken) => {
const accessToken: GithubAccessToken = {
...actualPAT,
_id: data.generateId(DataType.GithubAccessToken, personalTokenId),
_id: generateId(DataType.GithubAccessToken, personalTokenId),
$type: DataType.GithubAccessToken,
token: githubToken.access_token,
expiresIn: githubToken.expires_in,

View File

@@ -104,7 +104,12 @@ router.beforeEach(() => {
}
).startViewTransition(async () => {
resolve()
await nextTick()
await new Promise<void>((r) => {
const unwatch = router.afterEach(() => {
unwatch()
nextTick().then(r)
})
})
})
})
})

6
src/shims-vue.d.ts vendored
View File

@@ -1,3 +1,9 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module "pouchdb-adapter-indexeddb"
declare module "@toycode/markdown-it-class"
declare module "markdown-it-block-embed"

View File

@@ -6,6 +6,7 @@
:root {
--primary-color: #ffa4c0;
--font-family: "Libertinus Serif", serif;
--title-font-family: "Libertinus Serif", serif;
--font-size: 13pt;
--font-color: #4a4a4a;
--link: #445fb9;
@@ -24,8 +25,8 @@
@plugin 'daisyui' {
themes:
garden --default,
dim --prefersdark;
emerald --default,
forest --prefersdark;
}
@config '../../tailwind.config.js';
@@ -48,22 +49,10 @@
}
}
html {
overflow-y: auto;
overflow-x: auto;
}
html,
body {
height: 100vh;
height: 100dvh;
scroll-behavior: smooth;
overflow-y: auto;
}
@media screen and (min-width: 769px) {
html,
body {
overflow-y: hidden;
}
}
.columns {
@@ -77,7 +66,7 @@ body {
}
#app {
height: 100vh;
height: 100dvh;
display: flex;
}

View File

@@ -2,6 +2,6 @@
// Update these values to change the light and dark themes
export const themeConfig = {
light: "garden",
dark: "dim"
light: 'emerald',
dark: 'forest'
}

View File

@@ -6,7 +6,10 @@ const assembleFontLink = (font: string) => {
.replaceAll(" ", "+")}`
}
export const downloadFont = async (font: string): Promise<void> => {
export const downloadFont = async (
font: string,
cssVar = "--font-family"
): Promise<void> => {
const href = assembleFontLink(font)
// check if the href already exists
@@ -23,7 +26,7 @@ export const downloadFont = async (font: string): Promise<void> => {
try {
await new FontFaceObserver(font).load()
document.documentElement.style.setProperty("--font-family", font)
document.documentElement.style.setProperty(cssVar, font)
} catch (error) {
console.warn("error when loading font")
}

View File

@@ -16,6 +16,7 @@ import WelcomeWorld from "@/components/WelcomeWorld.vue"
flex: 1;
flex-direction: column;
align-items: center;
height: 100dvh;
}
.authorize {

View File

@@ -4,7 +4,7 @@ import { useTitle } from "@vueuse/core"
import { computed, nextTick, ref, watch } from "vue"
import { useRouter } from "vue-router"
import BackButton from "@/components/BackButton.vue"
import HomeButton from "@/components/HomeButton.vue"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
import StackedPublicNote from "@/components/StackedPublicNote.vue"
import ThemeSwap from "@/components/ThemeSwap.vue"
@@ -129,13 +129,7 @@ watch(
<main class="public-note-view repo-note note-container">
<div class="note article">
<div class="header">
<back-button
:fallback="{ name: 'PublicNoteListByDidView', params: { shortDid } }"
:prefer-fallback="false"
/>
<img src="/favicon.png" alt="Remanso" class="remanso-logo" />
<home-button />
<theme-swap />
</div>
<div class="subheader">
@@ -197,13 +191,6 @@ watch(
gap: 1rem;
}
.remanso-logo {
width: 32px;
height: 32px;
box-shadow: none;
view-transition-name: remanso-logo;
}
.subheader {
margin: 1rem auto 0;
}

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { vInfiniteScroll } from "@vueuse/components"
import GoBack from "@/components/GoBack.vue"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { useRepos } from "@/hooks/useRepos.hook"
@@ -6,12 +8,15 @@ import { useRepoList } from "@/modules/repo/hooks/useRepoList.hook"
const { username } = useGitHubLogin()
const { isReady } = useRepos()
const { favoriteRepos, otherRepos, favoriteCheckboxes, toggleCheckbox } =
const { favoriteRepos, otherRepos, favoriteCheckboxes, toggleCheckbox, canLoadMore, loadMore } =
useRepoList()
</script>
<template>
<div class="repo-list">
<div
class="repo-list"
v-infinite-scroll="[loadMore, { canLoadMore: () => canLoadMore }]"
>
<h1 class="title is-1">Repositories</h1>
<go-back />
<div v-if="!isReady">loading...</div>

View File

@@ -11,7 +11,8 @@ const defaultTitleStyles = Array.from(
...acc,
[heading]: {
"margin-top": "0",
"margin-bottom": "0.5em"
"margin-bottom": "0.5em",
"font-family": "var(--title-font-family)"
}
}),
{}

View File

@@ -47,11 +47,6 @@ export default defineConfig(({ command }) => {
sizes: "512x512",
type: "image/png"
},
{
src: "favicon.png",
sizes: "1024x1024",
type: "image/png"
},
{
src: "maskable-icon-512x512.png",
sizes: "512x512",