This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
root = "."
|
root = "."
|
||||||
tmp_dir = "_scratch/air"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -o ./_scratch/air/antholume ."
|
cmd = "go build -o ./tmp/antholume ."
|
||||||
full_bin = "./_scratch/air/antholume serve"
|
full_bin = "./tmp/antholume serve"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
include_ext = ["go", "tpl", "tmpl", "html", "css", "js", "yaml", "yml"]
|
include_ext = ["go", "tpl", "tmpl", "html", "css", "js", "yaml", "yml"]
|
||||||
exclude_dir = ["_scratch", "build", "data", "frontend", "node_modules"]
|
exclude_dir = ["_scratch", "build", "data", "frontend", "node_modules"]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Edit:
|
|||||||
|
|
||||||
Regenerate:
|
Regenerate:
|
||||||
- `go generate ./api/v1/generate.go`
|
- `go generate ./api/v1/generate.go`
|
||||||
- `cd frontend && bun run generate:api`
|
- `cd frontend && pnpm run generate:api`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- If you add response headers in `api/v1/openapi.yaml` (for example `Set-Cookie`), `oapi-codegen` will generate typed response header structs in `api/v1/api.gen.go`; update the handler response values to populate those headers explicitly.
|
- If you add response headers in `api/v1/openapi.yaml` (for example `Set-Cookie`), `oapi-codegen` will generate typed response header structs in `api/v1/api.gen.go`; update the handler response values to populate those headers explicitly.
|
||||||
@@ -51,7 +51,7 @@ Regenerate:
|
|||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- The Go server embeds `templates/*` and `assets/*`.
|
- The Go server embeds `templates/*` and `assets/*`.
|
||||||
- Root Tailwind output is built to `assets/style.css`.
|
- Root Tailwind output is built to `assets/style.css`. Two separate Tailwind setups exist: the legacy server-rendered app uses **Tailwind v3** via the root `tailwind.config.js` + `make legacy_tailwind`; the React app in `frontend/` uses **Tailwind v4** (CSS-first, no config file). Keep them distinct.
|
||||||
- Be mindful of whether a change affects the embedded server-rendered app, the React frontend, or both.
|
- Be mindful of whether a change affects the embedded server-rendered app, the React frontend, or both.
|
||||||
- SQLite timestamps are stored as RFC3339 strings (usually with a trailing `Z`); prefer `parseTime` / `parseTimePtr` instead of ad-hoc `time.Parse` layouts.
|
- SQLite timestamps are stored as RFC3339 strings (usually with a trailing `Z`); prefer `parseTime` / `parseTimePtr` instead of ad-hoc `time.Parse` layouts.
|
||||||
- `DISABLE_AUTH=true` bypasses authentication on **all** routes (v1 API, legacy web app, KOSync, OPDS). Set `DISABLE_AUTH_USER=<username>` to control which database user the session impersonates (defaults to the first user in the DB). The user must already exist.
|
- `DISABLE_AUTH=true` bypasses authentication on **all** routes (v1 API, legacy web app, KOSync, OPDS). Set `DISABLE_AUTH_USER=<username>` to control which database user the session impersonates (defaults to the first user in the DB). The user must already exist.
|
||||||
@@ -64,7 +64,7 @@ For frontend-specific implementation notes and commands, also read:
|
|||||||
## 6) Regeneration Summary
|
## 6) Regeneration Summary
|
||||||
|
|
||||||
- Go API: `go generate ./api/v1/generate.go`
|
- Go API: `go generate ./api/v1/generate.go`
|
||||||
- Frontend API client: `cd frontend && bun run generate:api`
|
- Frontend API client: `cd frontend && pnpm run generate:api`
|
||||||
- SQLC: `sqlc generate`
|
- SQLC: `sqlc generate`
|
||||||
|
|
||||||
## 7) Live Dev Server Debugging
|
## 7) Live Dev Server Debugging
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ dev_backend:
|
|||||||
$(DEV_ENV) air
|
$(DEV_ENV) air
|
||||||
|
|
||||||
dev_frontend:
|
dev_frontend:
|
||||||
cd frontend && bun run dev
|
cd frontend && pnpm run dev
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf ./build
|
rm -rf ./build
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
golangci-lint
|
golangci-lint
|
||||||
gopls
|
gopls
|
||||||
|
|
||||||
bun
|
|
||||||
nodejs
|
nodejs
|
||||||
|
pnpm
|
||||||
tailwindcss
|
tailwindcss
|
||||||
typescript-language-server
|
typescript-language-server
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"jsPlugins": ["oxlint-tailwindcss"],
|
||||||
|
"ignorePatterns": ["src/generated/**", "dist/**"],
|
||||||
|
"plugins": ["react", "typescript", "oxc"],
|
||||||
|
"settings": {
|
||||||
|
"tailwindcss": {
|
||||||
|
"entryPoint": "src/index.css"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": ["warn", { "allow": ["warn", "error"] }],
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/no-explicit-any": "warn",
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"tailwindcss/no-duplicate-classes": "error",
|
||||||
|
"tailwindcss/no-conflicting-classes": "error",
|
||||||
|
"tailwindcss/no-deprecated-classes": "error",
|
||||||
|
"tailwindcss/no-unnecessary-whitespace": "warn",
|
||||||
|
"tailwindcss/no-unknown-classes": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-16
@@ -5,11 +5,12 @@ Also follow the repository root guide at `../AGENTS.md`.
|
|||||||
|
|
||||||
## 1) Stack
|
## 1) Stack
|
||||||
|
|
||||||
- Package manager: `bun`
|
- Package manager: `pnpm`
|
||||||
- Framework: React + Vite
|
- Framework: React + Vite
|
||||||
- Data fetching: React Query
|
- Data fetching: React Query
|
||||||
- API generation: Orval
|
- API generation: Orval
|
||||||
- Linting: ESLint + Tailwind plugin
|
- Styling: Tailwind CSS v4 (CSS-first config)
|
||||||
|
- Linting: oxlint (config in `.oxlintrc.json`) + `oxlint-tailwindcss` for Tailwind rules
|
||||||
- Formatting: Prettier
|
- Formatting: Prettier
|
||||||
|
|
||||||
## 2) Conventions
|
## 2) Conventions
|
||||||
@@ -20,14 +21,15 @@ Also follow the repository root guide at `../AGENTS.md`.
|
|||||||
- Avoid custom class names in JSX `className` values unless the Tailwind lint config already allows them.
|
- Avoid custom class names in JSX `className` values unless the Tailwind lint config already allows them.
|
||||||
- For decorative icons in inputs or labels, disable hover styling via the icon component API rather than overriding it ad hoc.
|
- For decorative icons in inputs or labels, disable hover styling via the icon component API rather than overriding it ad hoc.
|
||||||
- Prefer `LoadingState` for result-area loading indicators; avoid early returns that unmount search/filter forms during fetches.
|
- Prefer `LoadingState` for result-area loading indicators; avoid early returns that unmount search/filter forms during fetches.
|
||||||
- Use theme tokens from `tailwind.config.js` / `src/index.css` (`bg-surface`, `text-content`, `border-border`, `primary`, etc.) for new UI work instead of adding raw light/dark color pairs.
|
- Use theme tokens defined in `src/index.css` `@theme` (`bg-surface`, `text-content`, `border-border`, `primary`, etc.) for new UI work instead of adding raw light/dark color pairs. There is no `tailwind.config.js` — Tailwind v4 config is CSS-first.
|
||||||
|
- Semantic colors map to runtime CSS variables (`--color-x: rgb(var(--x))`) via `@theme inline`; light/dark values live in `:root` / `.dark` in `src/index.css`. Dark mode is class-based via `@custom-variant dark`, toggled by `ThemeProvider`.
|
||||||
- Store frontend-only preferences in `src/utils/localSettings.ts` so appearance and view settings share one local-storage shape.
|
- Store frontend-only preferences in `src/utils/localSettings.ts` so appearance and view settings share one local-storage shape.
|
||||||
|
|
||||||
## 3) Generated API client
|
## 3) Generated API client
|
||||||
|
|
||||||
- Do not edit `src/generated/**` directly.
|
- Do not edit `src/generated/**` directly.
|
||||||
- Edit `../api/v1/openapi.yaml` and regenerate instead.
|
- Edit `../api/v1/openapi.yaml` and regenerate instead.
|
||||||
- Regenerate with: `bun run generate:api`
|
- Regenerate with: `pnpm run generate:api`
|
||||||
|
|
||||||
### Important behavior
|
### Important behavior
|
||||||
|
|
||||||
@@ -43,25 +45,27 @@ Also follow the repository root guide at `../AGENTS.md`.
|
|||||||
|
|
||||||
## 5) Commands
|
## 5) Commands
|
||||||
|
|
||||||
- Lint: `bun run lint`
|
- Lint: `pnpm run lint`
|
||||||
- Typecheck: `bun run typecheck`
|
- Typecheck: `pnpm run typecheck`
|
||||||
- Lint fix: `bun run lint:fix`
|
- Lint fix: `pnpm run lint:fix`
|
||||||
- Format check: `bun run format`
|
- Format check: `pnpm run format`
|
||||||
- Format fix: `bun run format:fix`
|
- Format fix: `pnpm run format:fix`
|
||||||
- Build: `bun run build`
|
- Build: `pnpm run build`
|
||||||
- Generate API client: `bun run generate:api`
|
- Generate API client: `pnpm run generate:api`
|
||||||
|
|
||||||
## 6) Validation Notes
|
## 6) Validation Notes
|
||||||
|
|
||||||
- ESLint ignores `src/generated/**`.
|
- oxlint ignores `src/generated/**` and `dist/**` (via `ignorePatterns` in `.oxlintrc.json`).
|
||||||
|
- `lint` runs `oxlint --max-warnings=0`; keep the tree warning-free. `react-hooks/exhaustive-deps` is enforced — fix deps rather than disabling the rule; use a justified inline `// oxlint-disable-next-line` only for genuine init-once effects.
|
||||||
|
- The Tailwind lint plugin uses oxlint `jsPlugins` (experimental, not run by the editor language server), so Tailwind diagnostics surface via CLI/CI, not in-editor. It reads the theme from `src/index.css` (`settings.tailwindcss.entryPoint`).
|
||||||
- Frontend unit tests use Vitest and live alongside source as `src/**/*.test.ts(x)`.
|
- Frontend unit tests use Vitest and live alongside source as `src/**/*.test.ts(x)`.
|
||||||
- Read `TESTING_STRATEGY.md` before adding or expanding frontend tests.
|
- Read `TESTING_STRATEGY.md` before adding or expanding frontend tests.
|
||||||
- Prefer tests for meaningful app behavior, branching logic, side effects, and user-visible outcomes.
|
- Prefer tests for meaningful app behavior, branching logic, side effects, and user-visible outcomes.
|
||||||
- Avoid low-value tests that mainly assert exact styling classes, duplicate existing coverage, or re-test framework/library behavior.
|
- Avoid low-value tests that mainly assert exact styling classes, duplicate existing coverage, or re-test framework/library behavior.
|
||||||
- `bun run lint` includes test files but does not typecheck.
|
- `pnpm run lint` includes test files but does not typecheck.
|
||||||
- Use `bun run typecheck` to run TypeScript validation for app code and colocated tests without a full production build.
|
- Use `pnpm run typecheck` to run TypeScript validation for app code and colocated tests without a full production build.
|
||||||
- Run frontend tests with `bun run test`.
|
- Run frontend tests with `pnpm run test`.
|
||||||
- `bun run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build.
|
- `pnpm run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build.
|
||||||
- When possible, validate changed files directly before escalating to full-project fixes.
|
- When possible, validate changed files directly before escalating to full-project fixes.
|
||||||
|
|
||||||
## 7) Live Dev Server Debugging
|
## 7) Live Dev Server Debugging
|
||||||
|
|||||||
+5
-5
@@ -68,7 +68,7 @@ The frontend mirrors the existing SSR templates structure:
|
|||||||
The frontend uses **Orval** to generate TypeScript types and React Query hooks from the OpenAPI spec:
|
The frontend uses **Orval** to generate TypeScript types and React Query hooks from the OpenAPI spec:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run generate:api
|
pnpm run generate:api
|
||||||
```
|
```
|
||||||
|
|
||||||
This generates:
|
This generates:
|
||||||
@@ -80,16 +80,16 @@ This generates:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
pnpm install
|
||||||
|
|
||||||
# Generate API types (if OpenAPI spec changes)
|
# Generate API types (if OpenAPI spec changes)
|
||||||
npm run generate:api
|
pnpm run generate:api
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
npm run dev
|
pnpm run dev
|
||||||
|
|
||||||
# Build for production
|
# Build for production
|
||||||
npm run build
|
pnpm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|||||||
@@ -68,6 +68,6 @@ Trim or remove tests that are brittle, duplicated, or mostly validate tooling ra
|
|||||||
|
|
||||||
For frontend test work, validate with:
|
For frontend test work, validate with:
|
||||||
|
|
||||||
- `cd frontend && bun run lint`
|
- `cd frontend && pnpm run lint`
|
||||||
- `cd frontend && bun run typecheck`
|
- `cd frontend && pnpm run typecheck`
|
||||||
- `cd frontend && bun run test`
|
- `cd frontend && pnpm run test`
|
||||||
|
|||||||
-1350
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
|||||||
import js from "@eslint/js";
|
|
||||||
import typescriptParser from "@typescript-eslint/parser";
|
|
||||||
import typescriptPlugin from "@typescript-eslint/eslint-plugin";
|
|
||||||
import reactPlugin from "eslint-plugin-react";
|
|
||||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
||||||
import tailwindcss from "eslint-plugin-tailwindcss";
|
|
||||||
import prettier from "eslint-plugin-prettier";
|
|
||||||
import eslintConfigPrettier from "eslint-config-prettier";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
js.configs.recommended,
|
|
||||||
{
|
|
||||||
files: ["**/*.ts", "**/*.tsx"],
|
|
||||||
ignores: ["**/generated/**"],
|
|
||||||
languageOptions: {
|
|
||||||
parser: typescriptParser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
projectService: true,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
localStorage: "readonly",
|
|
||||||
sessionStorage: "readonly",
|
|
||||||
document: "readonly",
|
|
||||||
window: "readonly",
|
|
||||||
setTimeout: "readonly",
|
|
||||||
clearTimeout: "readonly",
|
|
||||||
setInterval: "readonly",
|
|
||||||
clearInterval: "readonly",
|
|
||||||
HTMLElement: "readonly",
|
|
||||||
HTMLDivElement: "readonly",
|
|
||||||
HTMLButtonElement: "readonly",
|
|
||||||
HTMLAnchorElement: "readonly",
|
|
||||||
MouseEvent: "readonly",
|
|
||||||
Node: "readonly",
|
|
||||||
File: "readonly",
|
|
||||||
Blob: "readonly",
|
|
||||||
FormData: "readonly",
|
|
||||||
alert: "readonly",
|
|
||||||
confirm: "readonly",
|
|
||||||
prompt: "readonly",
|
|
||||||
React: "readonly",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"@typescript-eslint": typescriptPlugin,
|
|
||||||
react: reactPlugin,
|
|
||||||
"react-hooks": reactHooksPlugin,
|
|
||||||
tailwindcss,
|
|
||||||
prettier,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...eslintConfigPrettier.rules,
|
|
||||||
...tailwindcss.configs.recommended.rules,
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
|
||||||
"no-undef": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
varsIgnorePattern: "^_",
|
|
||||||
caughtErrorsIgnorePattern: "^_",
|
|
||||||
ignoreRestSiblings: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"no-useless-catch": "off",
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: "detect",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
+11
-14
@@ -9,8 +9,8 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate:api": "orval",
|
"generate:api": "orval",
|
||||||
"lint": "eslint src --max-warnings=0",
|
"lint": "oxlint --max-warnings=0",
|
||||||
"lint:fix": "eslint src --fix",
|
"lint:fix": "oxlint --fix",
|
||||||
"format": "prettier --check src",
|
"format": "prettier --check src",
|
||||||
"format:fix": "prettier --write src",
|
"format:fix": "prettier --write src",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
@@ -29,28 +29,25 @@
|
|||||||
"tailwind-merge": "^3.5.0"
|
"tailwind-merge": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.8",
|
"@types/react-dom": "^19.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
|
||||||
"@typescript-eslint/parser": "^8.13.0",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"eslint": "^9.17.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
|
||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
|
||||||
"jsdom": "^29.0.1",
|
"jsdom": "^29.0.1",
|
||||||
"postcss": "^8.4.49",
|
"oxlint": "^1.72.0",
|
||||||
|
"oxlint-tailwindcss": "^1.3.4",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^4.3.2",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.5",
|
"vite": "^6.0.5",
|
||||||
"vitest": "^4.1.0"
|
"vitest": "^4.1.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4233
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
allowBuilds:
|
||||||
|
core-js: set this to true or false
|
||||||
|
es5-ext: set this to true or false
|
||||||
|
esbuild: set this to true or false
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -27,7 +27,7 @@ export function Pagination({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPageChange(previousPage)}
|
onClick={() => onPageChange(previousPage)}
|
||||||
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
|
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-hidden"
|
||||||
>
|
>
|
||||||
◄
|
◄
|
||||||
</button>
|
</button>
|
||||||
@@ -41,7 +41,7 @@ export function Pagination({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPageChange(nextPage)}
|
onClick={() => onPageChange(nextPage)}
|
||||||
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
|
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-hidden"
|
||||||
>
|
>
|
||||||
►
|
►
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function Table<T extends object>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
|
||||||
<table className="min-w-full bg-surface">
|
<table className="min-w-full bg-surface">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border">
|
<tr className="border-b border-border">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { InfoIcon, WarningIcon, ErrorIcon, CloseIcon } from '../icons';
|
import { InfoIcon, WarningIcon, ErrorIcon, CloseIcon } from '../icons';
|
||||||
|
|
||||||
export type ToastType = 'info' | 'warning' | 'error';
|
export type ToastType = 'info' | 'warning' | 'error';
|
||||||
@@ -42,20 +42,20 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp
|
|||||||
|
|
||||||
const { baseStyles, typeStyles, iconStyles, textStyles } = getToastStyles(type);
|
const { baseStyles, typeStyles, iconStyles, textStyles } = getToastStyles(type);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = useCallback(() => {
|
||||||
setIsAnimatingOut(true);
|
setIsAnimatingOut(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
onClose?.(id);
|
onClose?.(id);
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
}, [id, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
const timer = setTimeout(handleClose, duration);
|
const timer = setTimeout(handleClose, duration);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [duration]);
|
}, [duration, handleClose]);
|
||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ export function useEpubReader({
|
|||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Init Reader Once - Theme values seed the constructor; the effect below (applyThemeChange) handles later changes, so re-running here would needlessly destroy and recreate the reader.
|
||||||
|
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [deviceId, deviceName, documentId, initialProgress, viewerNode]);
|
}, [deviceId, deviceName, documentId, initialProgress, viewerNode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+134
-3
@@ -1,6 +1,7 @@
|
|||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
/* Class-Based Dark Mode - v4 defaults to prefers-color-scheme; ThemeProvider toggles `.dark`. */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--white: 255 255 255;
|
--white: 255 255 255;
|
||||||
@@ -184,7 +185,137 @@
|
|||||||
--error-foreground: 255 255 255;
|
--error-foreground: 255 255 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-canvas: rgb(var(--canvas));
|
||||||
|
--color-surface: rgb(var(--surface));
|
||||||
|
--color-surface-muted: rgb(var(--surface-muted));
|
||||||
|
--color-surface-strong: rgb(var(--surface-strong));
|
||||||
|
--color-overlay: rgb(var(--overlay));
|
||||||
|
--color-content: rgb(var(--content));
|
||||||
|
--color-content-muted: rgb(var(--content-muted));
|
||||||
|
--color-content-subtle: rgb(var(--content-subtle));
|
||||||
|
--color-content-inverse: rgb(var(--content-inverse));
|
||||||
|
--color-border: rgb(var(--border));
|
||||||
|
--color-border-muted: rgb(var(--border-muted));
|
||||||
|
--color-border-strong: rgb(var(--border-strong));
|
||||||
|
--color-white: rgb(var(--white));
|
||||||
|
--color-black: rgb(var(--black));
|
||||||
|
|
||||||
|
--color-gray-50: rgb(var(--neutral-50));
|
||||||
|
--color-gray-100: rgb(var(--neutral-100));
|
||||||
|
--color-gray-200: rgb(var(--neutral-200));
|
||||||
|
--color-gray-300: rgb(var(--neutral-300));
|
||||||
|
--color-gray-400: rgb(var(--neutral-400));
|
||||||
|
--color-gray-500: rgb(var(--neutral-500));
|
||||||
|
--color-gray-600: rgb(var(--neutral-600));
|
||||||
|
--color-gray-700: rgb(var(--neutral-700));
|
||||||
|
--color-gray-800: rgb(var(--neutral-800));
|
||||||
|
--color-gray-900: rgb(var(--neutral-900));
|
||||||
|
--color-gray: rgb(var(--neutral-500));
|
||||||
|
--color-gray-foreground: rgb(var(--neutral-foreground));
|
||||||
|
|
||||||
|
--color-purple-50: rgb(var(--primary-50));
|
||||||
|
--color-purple-100: rgb(var(--primary-100));
|
||||||
|
--color-purple-200: rgb(var(--primary-200));
|
||||||
|
--color-purple-300: rgb(var(--primary-300));
|
||||||
|
--color-purple-400: rgb(var(--primary-400));
|
||||||
|
--color-purple-500: rgb(var(--primary-500));
|
||||||
|
--color-purple-600: rgb(var(--primary-600));
|
||||||
|
--color-purple-700: rgb(var(--primary-700));
|
||||||
|
--color-purple-800: rgb(var(--primary-800));
|
||||||
|
--color-purple-900: rgb(var(--primary-900));
|
||||||
|
--color-purple: rgb(var(--primary-500));
|
||||||
|
--color-purple-foreground: rgb(var(--primary-foreground));
|
||||||
|
|
||||||
|
--color-blue-50: rgb(var(--secondary-50));
|
||||||
|
--color-blue-100: rgb(var(--secondary-100));
|
||||||
|
--color-blue-200: rgb(var(--secondary-200));
|
||||||
|
--color-blue-300: rgb(var(--secondary-300));
|
||||||
|
--color-blue-400: rgb(var(--secondary-400));
|
||||||
|
--color-blue-500: rgb(var(--secondary-500));
|
||||||
|
--color-blue-600: rgb(var(--secondary-600));
|
||||||
|
--color-blue-700: rgb(var(--secondary-700));
|
||||||
|
--color-blue-800: rgb(var(--secondary-800));
|
||||||
|
--color-blue-900: rgb(var(--secondary-900));
|
||||||
|
--color-blue: rgb(var(--secondary-500));
|
||||||
|
--color-blue-foreground: rgb(var(--secondary-foreground));
|
||||||
|
|
||||||
|
--color-yellow-50: rgb(var(--warning-50));
|
||||||
|
--color-yellow-100: rgb(var(--warning-100));
|
||||||
|
--color-yellow-200: rgb(var(--warning-200));
|
||||||
|
--color-yellow-300: rgb(var(--warning-300));
|
||||||
|
--color-yellow-400: rgb(var(--warning-400));
|
||||||
|
--color-yellow-500: rgb(var(--warning-500));
|
||||||
|
--color-yellow-600: rgb(var(--warning-600));
|
||||||
|
--color-yellow-700: rgb(var(--warning-700));
|
||||||
|
--color-yellow-800: rgb(var(--warning-800));
|
||||||
|
--color-yellow-900: rgb(var(--warning-900));
|
||||||
|
--color-yellow: rgb(var(--warning-500));
|
||||||
|
--color-yellow-foreground: rgb(var(--warning-foreground));
|
||||||
|
|
||||||
|
--color-red-50: rgb(var(--error-50));
|
||||||
|
--color-red-100: rgb(var(--error-100));
|
||||||
|
--color-red-200: rgb(var(--error-200));
|
||||||
|
--color-red-300: rgb(var(--error-300));
|
||||||
|
--color-red-400: rgb(var(--error-400));
|
||||||
|
--color-red-500: rgb(var(--error-500));
|
||||||
|
--color-red-600: rgb(var(--error-600));
|
||||||
|
--color-red-700: rgb(var(--error-700));
|
||||||
|
--color-red-800: rgb(var(--error-800));
|
||||||
|
--color-red-900: rgb(var(--error-900));
|
||||||
|
--color-red: rgb(var(--error-500));
|
||||||
|
--color-red-foreground: rgb(var(--error-foreground));
|
||||||
|
|
||||||
|
--color-primary-50: rgb(var(--primary-50));
|
||||||
|
--color-primary-100: rgb(var(--primary-100));
|
||||||
|
--color-primary-200: rgb(var(--primary-200));
|
||||||
|
--color-primary-300: rgb(var(--primary-300));
|
||||||
|
--color-primary-400: rgb(var(--primary-400));
|
||||||
|
--color-primary-500: rgb(var(--primary-500));
|
||||||
|
--color-primary-600: rgb(var(--primary-600));
|
||||||
|
--color-primary-700: rgb(var(--primary-700));
|
||||||
|
--color-primary-800: rgb(var(--primary-800));
|
||||||
|
--color-primary-900: rgb(var(--primary-900));
|
||||||
|
--color-primary: rgb(var(--primary-500));
|
||||||
|
--color-primary-foreground: rgb(var(--primary-foreground));
|
||||||
|
|
||||||
|
--color-secondary-50: rgb(var(--secondary-50));
|
||||||
|
--color-secondary-100: rgb(var(--secondary-100));
|
||||||
|
--color-secondary-200: rgb(var(--secondary-200));
|
||||||
|
--color-secondary-300: rgb(var(--secondary-300));
|
||||||
|
--color-secondary-400: rgb(var(--secondary-400));
|
||||||
|
--color-secondary-500: rgb(var(--secondary-500));
|
||||||
|
--color-secondary-600: rgb(var(--secondary-600));
|
||||||
|
--color-secondary-700: rgb(var(--secondary-700));
|
||||||
|
--color-secondary-800: rgb(var(--secondary-800));
|
||||||
|
--color-secondary-900: rgb(var(--secondary-900));
|
||||||
|
--color-secondary: rgb(var(--secondary-500));
|
||||||
|
--color-secondary-foreground: rgb(var(--secondary-foreground));
|
||||||
|
|
||||||
|
--color-tertiary-50: rgb(var(--tertiary-50));
|
||||||
|
--color-tertiary-100: rgb(var(--tertiary-100));
|
||||||
|
--color-tertiary-200: rgb(var(--tertiary-200));
|
||||||
|
--color-tertiary-300: rgb(var(--tertiary-300));
|
||||||
|
--color-tertiary-400: rgb(var(--tertiary-400));
|
||||||
|
--color-tertiary-500: rgb(var(--tertiary-500));
|
||||||
|
--color-tertiary-600: rgb(var(--tertiary-600));
|
||||||
|
--color-tertiary-700: rgb(var(--tertiary-700));
|
||||||
|
--color-tertiary-800: rgb(var(--tertiary-800));
|
||||||
|
--color-tertiary-900: rgb(var(--tertiary-900));
|
||||||
|
--color-tertiary: rgb(var(--tertiary-500));
|
||||||
|
--color-tertiary-foreground: rgb(var(--tertiary-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
/* Preserve v3 Default Border Color - v4 changed the unqualified `border` utility to currentColor. */
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: rgb(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default function AdminImportPage() {
|
|||||||
if (selectedDirectory) {
|
if (selectedDirectory) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
|
||||||
<div className="flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
|
<div className="flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
|
||||||
<p className="text-lg font-semibold text-content">Selected Import Directory</p>
|
<p className="text-lg font-semibold text-content">Selected Import Directory</p>
|
||||||
<form className="flex flex-col gap-4" onSubmit={handleImport}>
|
<form className="flex flex-col gap-4" onSubmit={handleImport}>
|
||||||
@@ -122,7 +122,7 @@ export default function AdminImportPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
|
||||||
<table className="min-w-full bg-surface text-sm leading-normal text-content">
|
<table className="min-w-full bg-surface text-sm leading-normal text-content">
|
||||||
<thead className="text-content-muted">
|
<thead className="text-content-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -13,19 +13,15 @@ export default function AdminImportResultsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
|
||||||
<table className="min-w-full bg-surface text-sm leading-normal text-content">
|
<table className="min-w-full bg-surface text-sm leading-normal text-content">
|
||||||
<thead className="text-content-muted">
|
<thead className="text-content-muted">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
||||||
Document
|
Document
|
||||||
</th>
|
</th>
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
<th className="border-b border-border p-3 text-left font-normal uppercase">Status</th>
|
||||||
Status
|
<th className="border-b border-border p-3 text-left font-normal uppercase">Error</th>
|
||||||
</th>
|
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
|
||||||
Error
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -44,7 +40,10 @@ export default function AdminImportResultsPage() {
|
|||||||
>
|
>
|
||||||
<span className="text-content-muted">Name:</span>
|
<span className="text-content-muted">Name:</span>
|
||||||
{result.id ? (
|
{result.id ? (
|
||||||
<Link to={`/documents/${result.id}`} className="text-secondary-600 hover:underline">
|
<Link
|
||||||
|
to={`/documents/${result.id}`}
|
||||||
|
className="text-secondary-600 hover:underline"
|
||||||
|
>
|
||||||
{result.name}
|
{result.name}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ export default function AdminLogsPage() {
|
|||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
|
||||||
<div className="flex w-full grow flex-col">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<Search2Icon size={15} hoverable={false} />
|
<Search2Icon size={15} hoverable={false} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={e => setFilter(e.target.value)}
|
onChange={e => setFilter(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface p-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface p-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="JQ Filter"
|
placeholder="JQ Filter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function AdminUsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="min-w-full overflow-scroll rounded shadow">
|
<div className="min-w-full overflow-scroll rounded shadow-sm">
|
||||||
<table className="min-w-full bg-surface text-sm leading-normal text-content">
|
<table className="min-w-full bg-surface text-sm leading-normal text-content">
|
||||||
<thead className="text-content-muted">
|
<thead className="text-content-muted">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -163,7 +163,9 @@ export default function AdminUsersPage() {
|
|||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">User</th>
|
<th className="border-b border-border p-3 text-left font-normal uppercase">User</th>
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">Password</th>
|
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
||||||
|
Password
|
||||||
|
</th>
|
||||||
<th className="border-b border-border p-3 text-center font-normal uppercase">
|
<th className="border-b border-border p-3 text-center font-normal uppercase">
|
||||||
Permissions
|
Permissions
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function ComponentDemoPage() {
|
|||||||
<div className="space-y-8 p-4 text-content">
|
<div className="space-y-8 p-4 text-content">
|
||||||
<h1 className="text-2xl font-bold">UI Components Demo</h1>
|
<h1 className="text-2xl font-bold">UI Components Demo</h1>
|
||||||
|
|
||||||
<section className="rounded-lg bg-surface p-6 shadow">
|
<section className="rounded-lg bg-surface p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xl font-semibold">Toast Notifications</h2>
|
<h2 className="mb-4 text-xl font-semibold">Toast Notifications</h2>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<button
|
<button
|
||||||
@@ -72,7 +72,7 @@ export default function ComponentDemoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg bg-surface p-6 shadow">
|
<section className="rounded-lg bg-surface p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xl font-semibold">Skeleton Loading Components</h2>
|
<h2 className="mb-4 text-xl font-semibold">Skeleton Loading Components</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
@@ -115,7 +115,7 @@ export default function ComponentDemoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg bg-surface p-6 shadow">
|
<section className="rounded-lg bg-surface p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xl font-semibold">Skeleton Cards</h2>
|
<h2 className="mb-4 text-xl font-semibold">Skeleton Cards</h2>
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<SkeletonCard />
|
<SkeletonCard />
|
||||||
@@ -124,17 +124,17 @@ export default function ComponentDemoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg bg-surface p-6 shadow">
|
<section className="rounded-lg bg-surface p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xl font-semibold">Skeleton Table</h2>
|
<h2 className="mb-4 text-xl font-semibold">Skeleton Table</h2>
|
||||||
<SkeletonTable rows={5} columns={4} />
|
<SkeletonTable rows={5} columns={4} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg bg-surface p-6 shadow">
|
<section className="rounded-lg bg-surface p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xl font-semibold">Page Loader</h2>
|
<h2 className="mb-4 text-xl font-semibold">Page Loader</h2>
|
||||||
<PageLoader message="Loading demo content..." />
|
<PageLoader message="Loading demo content..." />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg bg-surface p-6 shadow">
|
<section className="rounded-lg bg-surface p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xl font-semibold">Inline Loader</h2>
|
<h2 className="mb-4 text-xl font-semibold">Inline Loader</h2>
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ import {
|
|||||||
import { Field, FieldLabel, FieldValue, FieldActions } from '../components';
|
import { Field, FieldLabel, FieldValue, FieldActions } from '../components';
|
||||||
|
|
||||||
const iconButtonClassName = 'cursor-pointer text-content-muted hover:text-content';
|
const iconButtonClassName = 'cursor-pointer text-content-muted hover:text-content';
|
||||||
const popupClassName = 'rounded bg-surface-strong p-3 text-content shadow-lg transition-all duration-200';
|
const popupClassName =
|
||||||
|
'rounded bg-surface-strong p-3 text-content shadow-lg transition-all duration-200';
|
||||||
const popupInputClassName = 'rounded bg-surface p-2 text-content';
|
const popupInputClassName = 'rounded bg-surface p-2 text-content';
|
||||||
const editInputClassName =
|
const editInputClassName =
|
||||||
'w-full rounded border border-secondary-200 bg-secondary-50 p-2 text-lg font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500';
|
'w-full rounded border border-secondary-200 bg-secondary-50 p-2 text-lg font-medium text-content focus:outline-hidden focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500';
|
||||||
|
|
||||||
export default function DocumentPage() {
|
export default function DocumentPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -122,7 +123,7 @@ export default function DocumentPage() {
|
|||||||
{document.filepath && (
|
{document.filepath && (
|
||||||
<a
|
<a
|
||||||
href={`/reader/${document.id}`}
|
href={`/reader/${document.id}`}
|
||||||
className="z-10 mt-2 w-full rounded bg-secondary-700 py-1 text-center text-sm font-medium text-white hover:bg-secondary-800 focus:outline-none focus:ring-4 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700"
|
className="z-10 mt-2 w-full rounded bg-secondary-700 py-1 text-center text-sm font-medium text-white hover:bg-secondary-800 focus:outline-hidden focus:ring-4 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700"
|
||||||
>
|
>
|
||||||
Read
|
Read
|
||||||
</a>
|
</a>
|
||||||
@@ -154,7 +155,12 @@ export default function DocumentPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<form className="flex w-72 flex-col gap-2 text-sm">
|
<form className="flex w-72 flex-col gap-2 text-sm">
|
||||||
<input type="file" id="cover_file" name="cover_file" className={popupInputClassName} />
|
<input
|
||||||
|
type="file"
|
||||||
|
id="cover_file"
|
||||||
|
name="cover_file"
|
||||||
|
className={popupInputClassName}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
|
className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
|
||||||
@@ -163,7 +169,13 @@ export default function DocumentPage() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form className="flex w-72 flex-col gap-2 text-sm">
|
<form className="flex w-72 flex-col gap-2 text-sm">
|
||||||
<input type="checkbox" checked id="remove_cover" name="remove_cover" className="hidden" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked
|
||||||
|
id="remove_cover"
|
||||||
|
name="remove_cover"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
|
className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
|
||||||
@@ -283,10 +295,20 @@ export default function DocumentPage() {
|
|||||||
<FieldActions>
|
<FieldActions>
|
||||||
{isEditingTitle ? (
|
{isEditingTitle ? (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<button type="button" onClick={() => setIsEditingTitle(false)} className={iconButtonClassName} aria-label="Cancel edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditingTitle(false)}
|
||||||
|
className={iconButtonClassName}
|
||||||
|
aria-label="Cancel edit"
|
||||||
|
>
|
||||||
<CloseIcon size={18} />
|
<CloseIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={saveTitle} className={iconButtonClassName} aria-label="Confirm edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveTitle}
|
||||||
|
className={iconButtonClassName}
|
||||||
|
aria-label="Confirm edit"
|
||||||
|
>
|
||||||
<CheckIcon size={18} />
|
<CheckIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,7 +331,12 @@ export default function DocumentPage() {
|
|||||||
>
|
>
|
||||||
{isEditingTitle ? (
|
{isEditingTitle ? (
|
||||||
<div className="relative mt-1 flex gap-2">
|
<div className="relative mt-1 flex gap-2">
|
||||||
<input type="text" value={editTitle} onChange={e => setEditTitle(e.target.value)} className={editInputClassName} />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editTitle}
|
||||||
|
onChange={e => setEditTitle(e.target.value)}
|
||||||
|
className={editInputClassName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FieldValue>{document.title}</FieldValue>
|
<FieldValue>{document.title}</FieldValue>
|
||||||
@@ -324,10 +351,20 @@ export default function DocumentPage() {
|
|||||||
<FieldActions>
|
<FieldActions>
|
||||||
{isEditingAuthor ? (
|
{isEditingAuthor ? (
|
||||||
<>
|
<>
|
||||||
<button type="button" onClick={() => setIsEditingAuthor(false)} className={iconButtonClassName} aria-label="Cancel edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditingAuthor(false)}
|
||||||
|
className={iconButtonClassName}
|
||||||
|
aria-label="Cancel edit"
|
||||||
|
>
|
||||||
<CloseIcon size={18} />
|
<CloseIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={saveAuthor} className={iconButtonClassName} aria-label="Confirm edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveAuthor}
|
||||||
|
className={iconButtonClassName}
|
||||||
|
aria-label="Confirm edit"
|
||||||
|
>
|
||||||
<CheckIcon size={18} />
|
<CheckIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@@ -350,7 +387,12 @@ export default function DocumentPage() {
|
|||||||
>
|
>
|
||||||
{isEditingAuthor ? (
|
{isEditingAuthor ? (
|
||||||
<div className="relative mt-1 flex gap-2">
|
<div className="relative mt-1 flex gap-2">
|
||||||
<input type="text" value={editAuthor} onChange={e => setEditAuthor(e.target.value)} className={editInputClassName} />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editAuthor}
|
||||||
|
onChange={e => setEditAuthor(e.target.value)}
|
||||||
|
className={editInputClassName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FieldValue>{document.author}</FieldValue>
|
<FieldValue>{document.author}</FieldValue>
|
||||||
@@ -376,11 +418,15 @@ export default function DocumentPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex text-xs">
|
<div className="flex text-xs">
|
||||||
<p className="w-32 text-content-subtle">Seconds / Percent</p>
|
<p className="w-32 text-content-subtle">Seconds / Percent</p>
|
||||||
<p className="font-medium">{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}</p>
|
<p className="font-medium">
|
||||||
|
{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex text-xs">
|
<div className="flex text-xs">
|
||||||
<p className="w-32 text-content-subtle">Words / Minute</p>
|
<p className="w-32 text-content-subtle">Words / Minute</p>
|
||||||
<p className="font-medium">{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}</p>
|
<p className="font-medium">
|
||||||
|
{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex text-xs">
|
<div className="flex text-xs">
|
||||||
<p className="w-32 text-content-subtle">Est. Time Left</p>
|
<p className="w-32 text-content-subtle">Est. Time Left</p>
|
||||||
@@ -412,10 +458,20 @@ export default function DocumentPage() {
|
|||||||
<FieldActions>
|
<FieldActions>
|
||||||
{isEditingDescription ? (
|
{isEditingDescription ? (
|
||||||
<>
|
<>
|
||||||
<button type="button" onClick={() => setIsEditingDescription(false)} className={iconButtonClassName} aria-label="Cancel edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditingDescription(false)}
|
||||||
|
className={iconButtonClassName}
|
||||||
|
aria-label="Cancel edit"
|
||||||
|
>
|
||||||
<CloseIcon size={18} />
|
<CloseIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={saveDescription} className={iconButtonClassName} aria-label="Confirm edit">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveDescription}
|
||||||
|
className={iconButtonClassName}
|
||||||
|
aria-label="Confirm edit"
|
||||||
|
>
|
||||||
<CheckIcon size={18} />
|
<CheckIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@@ -441,12 +497,14 @@ export default function DocumentPage() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={editDescription}
|
value={editDescription}
|
||||||
onChange={e => setEditDescription(e.target.value)}
|
onChange={e => setEditDescription(e.target.value)}
|
||||||
className="h-32 w-full grow rounded border border-secondary-200 bg-secondary-50 p-2 font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500"
|
className="h-32 w-full grow rounded border border-secondary-200 bg-secondary-50 p-2 font-medium text-content focus:outline-hidden focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500"
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FieldValue className="hyphens-auto text-justify">{document.description || 'N/A'}</FieldValue>
|
<FieldValue className="hyphens-auto text-justify">
|
||||||
|
{document.description || 'N/A'}
|
||||||
|
</FieldValue>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
<div
|
<div
|
||||||
role="link"
|
role="link"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="flex size-full cursor-pointer gap-4 rounded bg-surface p-4 shadow-lg transition-colors hover:bg-surface-muted focus:outline-none"
|
className="flex size-full cursor-pointer gap-4 rounded bg-surface p-4 shadow-lg transition-colors hover:bg-surface-muted focus:outline-hidden"
|
||||||
onClick={() => navigate(`/documents/${doc.id}`)}
|
onClick={() => navigate(`/documents/${doc.id}`)}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
@@ -100,7 +100,7 @@ function DocumentListItem({ doc }: DocumentListItemProps) {
|
|||||||
<div
|
<div
|
||||||
role="link"
|
role="link"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="block cursor-pointer rounded bg-surface p-4 text-content shadow-lg transition-colors hover:bg-surface-muted focus:outline-none"
|
className="block cursor-pointer rounded bg-surface p-4 text-content shadow-lg transition-colors hover:bg-surface-muted focus:outline-hidden"
|
||||||
onClick={() => navigate(`/documents/${doc.id}`)}
|
onClick={() => navigate(`/documents/${doc.id}`)}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
@@ -214,14 +214,14 @@ export default function DocumentsPage() {
|
|||||||
<div className="flex flex-col gap-4 lg:flex-row">
|
<div className="flex flex-col gap-4 lg:flex-row">
|
||||||
<div className="flex w-full grow flex-col">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<Search2Icon size={15} hoverable={false} />
|
<Search2Icon size={15} hoverable={false} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Search Author / Title"
|
placeholder="Search Author / Title"
|
||||||
name="search"
|
name="search"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function LoginPageView({
|
|||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={e => onUsernameChange(e.target.value)}
|
onChange={e => onUsernameChange(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
required
|
required
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -69,7 +69,7 @@ export function LoginPageView({
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => onPasswordChange(e.target.value)}
|
onChange={e => onPasswordChange(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
required
|
required
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -80,7 +80,7 @@ export function LoginPageView({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-none focus:ring-2 disabled:opacity-50"
|
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-hidden focus:ring-2 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Logging in...' : 'Login'}
|
{isLoading ? 'Logging in...' : 'Login'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ export default function ReaderPage() {
|
|||||||
colorScheme,
|
colorScheme,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
isPaginationDisabled: useCallback(() => isTopBarOpen || isBottomBarOpen, [isTopBarOpen, isBottomBarOpen]),
|
isPaginationDisabled: useCallback(
|
||||||
|
() => isTopBarOpen || isBottomBarOpen,
|
||||||
|
[isTopBarOpen, isBottomBarOpen]
|
||||||
|
),
|
||||||
onSwipeDown: handleSwipeDown,
|
onSwipeDown: handleSwipeDown,
|
||||||
onSwipeUp: handleSwipeUp,
|
onSwipeUp: handleSwipeUp,
|
||||||
onCenterTap: handleCenterTap,
|
onCenterTap: handleCenterTap,
|
||||||
@@ -86,9 +89,10 @@ export default function ReaderPage() {
|
|||||||
}
|
}
|
||||||
}, [document?.title]);
|
}, [document?.title]);
|
||||||
|
|
||||||
|
const { setTheme } = reader;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reader.setTheme({ colorScheme, fontFamily, fontSize });
|
setTheme({ colorScheme, fontFamily, fontSize });
|
||||||
}, [colorScheme, fontFamily, fontSize, reader.setTheme]);
|
}, [colorScheme, fontFamily, fontSize, setTheme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTopBarOpen || isBottomBarOpen) {
|
if (isTopBarOpen || isBottomBarOpen) {
|
||||||
@@ -122,7 +126,7 @@ export default function ReaderPage() {
|
|||||||
<div className="flex min-w-0 items-start gap-4">
|
<div className="flex min-w-0 items-start gap-4">
|
||||||
<Link to={`/documents/${document.id}`} className="block shrink-0">
|
<Link to={`/documents/${document.id}`} className="block shrink-0">
|
||||||
<img
|
<img
|
||||||
className="h-28 w-20 rounded object-cover shadow"
|
className="h-28 w-20 rounded object-cover shadow-sm"
|
||||||
src={`/api/v1/documents/${document.id}/cover`}
|
src={`/api/v1/documents/${document.id}/cover`}
|
||||||
alt={`${document.title} cover`}
|
alt={`${document.title} cover`}
|
||||||
/>
|
/>
|
||||||
@@ -216,7 +220,9 @@ export default function ReaderPage() {
|
|||||||
|
|
||||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto] lg:items-start">
|
<div className="grid gap-3 lg:grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto] lg:items-start">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="mb-1 text-[10px] uppercase tracking-wide text-content-subtle">Theme</p>
|
<p className="mb-1 text-[10px] uppercase tracking-wide text-content-subtle">
|
||||||
|
Theme
|
||||||
|
</p>
|
||||||
<div className="grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 lg:grid-cols-5">
|
<div className="grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 lg:grid-cols-5">
|
||||||
{colorSchemes.map(option => (
|
{colorSchemes.map(option => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function RegisterPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={e => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
required
|
required
|
||||||
disabled={isLoading || isLoadingInfo || !registrationEnabled}
|
disabled={isLoading || isLoadingInfo || !registrationEnabled}
|
||||||
@@ -74,7 +74,7 @@ export default function RegisterPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
required
|
required
|
||||||
disabled={isLoading || isLoadingInfo || !registrationEnabled}
|
disabled={isLoading || isLoadingInfo || !registrationEnabled}
|
||||||
@@ -85,7 +85,7 @@ export default function RegisterPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading || isLoadingInfo || !registrationEnabled}
|
disabled={isLoading || isLoadingInfo || !registrationEnabled}
|
||||||
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-none focus:ring-2 disabled:opacity-50"
|
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-hidden focus:ring-2 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Registering...' : 'Register'}
|
{isLoading ? 'Registering...' : 'Register'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -53,26 +53,26 @@ export function SearchPageView({
|
|||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={onSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={onSubmit}>
|
||||||
<div className="flex w-full grow flex-col">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<Search2Icon size={15} hoverable={false} />
|
<Search2Icon size={15} hoverable={false} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => onQueryChange(e.target.value)}
|
onChange={e => onQueryChange(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Query"
|
placeholder="Query"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex min-w-[12em]">
|
<div className="relative flex min-w-[12em]">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<BookIcon size={15} />
|
<BookIcon size={15} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={source}
|
value={source}
|
||||||
onChange={e => onSourceChange(e.target.value as GetSearchSource)}
|
onChange={e => onSourceChange(e.target.value as GetSearchSource)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
>
|
>
|
||||||
<option value={GetSearchSource.LibGen}>Library Genesis</option>
|
<option value={GetSearchSource.LibGen}>Library Genesis</option>
|
||||||
<option value={GetSearchSource.Annas_Archive}>Annas Archive</option>
|
<option value={GetSearchSource.Annas_Archive}>Annas Archive</option>
|
||||||
@@ -86,7 +86,7 @@ export function SearchPageView({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
|
||||||
<table className="min-w-full bg-surface text-sm leading-normal text-content md:text-sm">
|
<table className="min-w-full bg-surface text-sm leading-normal text-content md:text-sm">
|
||||||
<thead className="text-content-muted">
|
<thead className="text-content-muted">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -97,12 +97,8 @@ export function SearchPageView({
|
|||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
||||||
Series
|
Series
|
||||||
</th>
|
</th>
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
<th className="border-b border-border p-3 text-left font-normal uppercase">Type</th>
|
||||||
Type
|
<th className="border-b border-border p-3 text-left font-normal uppercase">Size</th>
|
||||||
</th>
|
|
||||||
<th className="border-b border-border p-3 text-left font-normal uppercase">
|
|
||||||
Size
|
|
||||||
</th>
|
|
||||||
<th className="hidden border-b border-border p-3 text-left font-normal uppercase md:table-cell">
|
<th className="hidden border-b border-border p-3 text-left font-normal uppercase md:table-cell">
|
||||||
Date
|
Date
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -143,28 +143,28 @@ export default function SettingsPage() {
|
|||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handlePasswordSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handlePasswordSubmit}>
|
||||||
<div className="flex grow flex-col">
|
<div className="flex grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<PasswordIcon size={15} />
|
<PasswordIcon size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex grow flex-col">
|
<div className="flex grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<PasswordIcon size={15} />
|
<PasswordIcon size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={e => setNewPassword(e.target.value)}
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
placeholder="New Password"
|
placeholder="New Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,13 +220,13 @@ export default function SettingsPage() {
|
|||||||
<p className="mb-2 text-lg font-semibold text-content">Change Timezone</p>
|
<p className="mb-2 text-lg font-semibold text-content">Change Timezone</p>
|
||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleTimezoneSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleTimezoneSubmit}>
|
||||||
<div className="relative flex grow">
|
<div className="relative flex grow">
|
||||||
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
|
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
|
||||||
<ClockIcon size={15} />
|
<ClockIcon size={15} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={timezone || 'UTC'}
|
value={timezone || 'UTC'}
|
||||||
onChange={e => setTimezone(e.target.value)}
|
onChange={e => setTimezone(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
|
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
|
||||||
>
|
>
|
||||||
<option value="UTC">UTC</option>
|
<option value="UTC">UTC</option>
|
||||||
<option value="America/New_York">America/New_York</option>
|
<option value="America/New_York">America/New_York</option>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
const withOpacity = cssVariable => `rgb(var(${cssVariable}) / <alpha-value>)`;
|
|
||||||
|
|
||||||
const buildScale = scaleName => ({
|
|
||||||
50: withOpacity(`--${scaleName}-50`),
|
|
||||||
100: withOpacity(`--${scaleName}-100`),
|
|
||||||
200: withOpacity(`--${scaleName}-200`),
|
|
||||||
300: withOpacity(`--${scaleName}-300`),
|
|
||||||
400: withOpacity(`--${scaleName}-400`),
|
|
||||||
500: withOpacity(`--${scaleName}-500`),
|
|
||||||
600: withOpacity(`--${scaleName}-600`),
|
|
||||||
700: withOpacity(`--${scaleName}-700`),
|
|
||||||
800: withOpacity(`--${scaleName}-800`),
|
|
||||||
900: withOpacity(`--${scaleName}-900`),
|
|
||||||
DEFAULT: withOpacity(`--${scaleName}-500`),
|
|
||||||
foreground: withOpacity(`--${scaleName}-foreground`),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
canvas: withOpacity('--canvas'),
|
|
||||||
surface: withOpacity('--surface'),
|
|
||||||
'surface-muted': withOpacity('--surface-muted'),
|
|
||||||
'surface-strong': withOpacity('--surface-strong'),
|
|
||||||
overlay: withOpacity('--overlay'),
|
|
||||||
content: withOpacity('--content'),
|
|
||||||
'content-muted': withOpacity('--content-muted'),
|
|
||||||
'content-subtle': withOpacity('--content-subtle'),
|
|
||||||
'content-inverse': withOpacity('--content-inverse'),
|
|
||||||
border: withOpacity('--border'),
|
|
||||||
'border-muted': withOpacity('--border-muted'),
|
|
||||||
'border-strong': withOpacity('--border-strong'),
|
|
||||||
white: withOpacity('--white'),
|
|
||||||
black: withOpacity('--black'),
|
|
||||||
gray: buildScale('neutral'),
|
|
||||||
purple: buildScale('primary'),
|
|
||||||
blue: buildScale('secondary'),
|
|
||||||
yellow: buildScale('warning'),
|
|
||||||
red: buildScale('error'),
|
|
||||||
primary: buildScale('primary'),
|
|
||||||
secondary: buildScale('secondary'),
|
|
||||||
tertiary: buildScale('tertiary'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['lin-va-terminal'],
|
allowedHosts: ['lin-va-terminal'],
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user