diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml index 0adeade..41114cd 100644 --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -2,9 +2,9 @@ name: Notify on: push: branches: - - "v2" - - "stable" - - "dev" + - 'v2' + - 'stable' + - 'dev' jobs: call-notify-commit: uses: Dictionarry-Hub/parrot/.github/workflows/notify-commit.yml@v1 diff --git a/deno.json b/deno.json index d28d17b..ee3acc7 100644 --- a/deno.json +++ b/deno.json @@ -1,55 +1,58 @@ { - "imports": { - "$lib/": "./src/lib/", - "$api/": "./src/lib/api/", - "$config": "./src/lib/server/utils/config/config.ts", - "$logger/": "./src/lib/server/utils/logger/", - "$shared/": "./src/lib/shared/", - "$stores/": "./src/lib/client/stores/", - "$ui/": "./src/lib/client/ui/", - "$assets/": "./src/lib/client/assets/", - "$alerts/": "./src/lib/client/alerts/", - "$server/": "./src/server/", - "$db/": "./src/lib/server/db/", - "$jobs/": "./src/lib/server/jobs/", - "$pcd/": "./src/lib/server/pcd/", - "$arr/": "./src/lib/server/utils/arr/", - "$http/": "./src/lib/server/utils/http/", - "$utils/": "./src/lib/server/utils/", - "$notifications/": "./src/lib/server/notifications/", - "$cache/": "./src/lib/server/utils/cache/", - "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0", - "@std/assert": "jsr:@std/assert@^1.0.0", - "marked": "npm:marked@^15.0.6", - "simple-icons": "npm:simple-icons@^15.17.0", - "highlight.js": "npm:highlight.js@^11.11.1", - "croner": "npm:croner@^8.1.2", - "@std/yaml": "jsr:@std/yaml@^1.0.10" - }, - "tasks": { - "dev": "deno run -A scripts/dev.ts", - "dev:vite": "DENO_ENV=development PORT=6969 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev", - "dev:parser": "cd src/services/parser && dotnet watch run --urls http://localhost:5000", - "build": "APP_BASE_PATH=./dist/build deno run -A npm:vite build && deno compile --no-check --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-run --allow-sys --target x86_64-unknown-linux-gnu --output dist/build/profilarr dist/build/mod.ts", - "build:windows": "APP_BASE_PATH=./dist/build deno run -A npm:vite build && deno compile --no-check --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-run --allow-sys --target x86_64-pc-windows-msvc --output dist/windows/profilarr.exe dist/build/mod.ts", - "preview": "PORT=6868 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 ./dist/build/profilarr", - "format": "prettier --write .", - "lint": "prettier --check . && eslint .", - "test": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env", - "test:watch": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env --watch", - "generate:api-types": "npx openapi-typescript docs/api/v1/openapi.yaml -o src/lib/api/v1.d.ts" - }, - "compilerOptions": { - "lib": ["deno.window", "dom"], - "strict": true - }, - "exclude": ["dist/", "node_modules/"], - "fmt": { - "exclude": ["dist/", "node_modules/"], - "indentWidth": 2, - "useTabs": false - }, - "lint": { - "exclude": ["dist/", "node_modules/"] - } + "imports": { + "$lib/": "./src/lib/", + "$api/": "./src/lib/api/", + "$config": "./src/lib/server/utils/config/config.ts", + "$logger/": "./src/lib/server/utils/logger/", + "$shared/": "./src/lib/shared/", + "$stores/": "./src/lib/client/stores/", + "$ui/": "./src/lib/client/ui/", + "$assets/": "./src/lib/client/assets/", + "$alerts/": "./src/lib/client/alerts/", + "$server/": "./src/server/", + "$db/": "./src/lib/server/db/", + "$jobs/": "./src/lib/server/jobs/", + "$pcd/": "./src/lib/server/pcd/", + "$arr/": "./src/lib/server/utils/arr/", + "$http/": "./src/lib/server/utils/http/", + "$utils/": "./src/lib/server/utils/", + "$notifications/": "./src/lib/server/notifications/", + "$cache/": "./src/lib/server/utils/cache/", + "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0", + "@std/assert": "jsr:@std/assert@^1.0.0", + "marked": "npm:marked@^15.0.6", + "simple-icons": "npm:simple-icons@^15.17.0", + "highlight.js": "npm:highlight.js@^11.11.1", + "croner": "npm:croner@^8.1.2", + "@std/yaml": "jsr:@std/yaml@^1.0.10" + }, + "tasks": { + "dev": "deno run -A scripts/dev.ts", + "dev:vite": "DENO_ENV=development PORT=6969 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev", + "dev:parser": "cd src/services/parser && dotnet watch run --urls http://localhost:5000", + "build": "APP_BASE_PATH=./dist/build deno run -A npm:vite build && deno compile --no-check --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-run --allow-sys --target x86_64-unknown-linux-gnu --output dist/build/profilarr dist/build/mod.ts", + "build:windows": "APP_BASE_PATH=./dist/build deno run -A npm:vite build && deno compile --no-check --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-run --allow-sys --target x86_64-pc-windows-msvc --output dist/windows/profilarr.exe dist/build/mod.ts", + "preview": "PORT=6868 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 ./dist/build/profilarr", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "check": "deno task check:server && deno task check:client", + "check:server": "deno check src/lib/server/**/*.ts", + "check:client": "npx svelte-check --tsconfig ./tsconfig.json", + "test": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env", + "test:watch": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env --watch", + "generate:api-types": "npx openapi-typescript docs/api/v1/openapi.yaml -o src/lib/api/v1.d.ts" + }, + "compilerOptions": { + "lib": ["deno.window", "dom"], + "strict": true + }, + "exclude": ["dist/", "node_modules/", "src/routes/"], + "fmt": { + "exclude": ["dist/", "node_modules/"], + "indentWidth": 2, + "useTabs": false + }, + "lint": { + "exclude": ["dist/", "node_modules/"] + } } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 96949d6..ff5d721 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -226,23 +226,23 @@ radius. Adding spacing breaks the connected appearance. ```svelte - - - + + + - - + + -
- -
- +
+ +
+
``` @@ -368,7 +368,7 @@ centralizes all application paths and environment configuration. Import it via `$config`. ```typescript -import { config } from "$config"; +import { config } from '$config'; // Paths config.paths.base; // Application root @@ -397,12 +397,12 @@ The logger (`logger/logger.ts`) handles console and file output with daily rotation. Import via `$logger/logger.ts`. ```typescript -import { logger } from "$logger/logger.ts"; +import { logger } from '$logger/logger.ts'; -await logger.debug("Cache miss", { source: "PCD", meta: { id: 1 } }); -await logger.info("Sync completed", { source: "Sync" }); -await logger.warn("Rate limited", { source: "GitHub" }); -await logger.error("Connection failed", { source: "Arr", meta: error }); +await logger.debug('Cache miss', { source: 'PCD', meta: { id: 1 } }); +await logger.info('Sync completed', { source: 'Sync' }); +await logger.warn('Rate limited', { source: 'GitHub' }); +await logger.error('Connection failed', { source: 'Arr', meta: error }); ``` Log levels: DEBUG → INFO → WARN → ERROR. Users configure the minimum level in @@ -426,13 +426,13 @@ verbose messages or logging the same event multiple times. ```typescript // Good -await logger.info("Synced 5 profiles to Radarr", { source: "Sync" }); +await logger.info('Synced 5 profiles to Radarr', { source: 'Sync' }); // Bad - too verbose, no source -await logger.info("Starting to sync profiles now..."); -await logger.info("Found 5 profiles to sync"); -await logger.info("Syncing profile 1..."); -await logger.info("Syncing profile 2..."); +await logger.info('Starting to sync profiles now...'); +await logger.info('Found 5 profiles to sync'); +await logger.info('Syncing profile 1...'); +await logger.info('Syncing profile 2...'); ``` **HTTP** @@ -441,21 +441,21 @@ The HTTP client (`http/client.ts`) provides a base class with connection pooling and retry logic. Arr clients extend this. ```typescript -import { BaseHttpClient } from "$http/client.ts"; +import { BaseHttpClient } from '$http/client.ts'; class MyClient extends BaseHttpClient { - constructor(url: string) { - super(url, { - timeout: 30000, // Request timeout (ms) - retries: 3, // Retry count for 5xx errors - retryDelay: 500, // Base delay (exponential backoff) - headers: { Authorization: "Bearer token" }, - }); - } + constructor(url: string) { + super(url, { + timeout: 30000, // Request timeout (ms) + retries: 3, // Retry count for 5xx errors + retryDelay: 500, // Base delay (exponential backoff) + headers: { Authorization: 'Bearer token' } + }); + } } -const client = new MyClient("https://api.example.com"); -const data = await client.get("/endpoint"); +const client = new MyClient('https://api.example.com'); +const data = await client.get('/endpoint'); client.close(); // Release connection pool ``` @@ -476,15 +476,15 @@ Git operations (`git/`) wrap command-line git for PCD repository management. The `Git` class (`Git.ts`) provides a clean interface per repository: ```typescript -import { Git } from "$utils/git/index.ts"; +import { Git } from '$utils/git/index.ts'; -const git = new Git("/path/to/repo"); +const git = new Git('/path/to/repo'); // Repository operations await git.fetch(); await git.pull(); await git.push(); -await git.checkout("main"); +await git.checkout('main'); await git.resetToRemote(); // Status queries @@ -497,7 +497,7 @@ const commits = await git.getCommits(10); const uncommitted = await git.getUncommittedOps(); const maxOp = await git.getMaxOpNumber(); await git.discardOps(filepaths); -await git.addOps(filepaths, "commit message"); +await git.addOps(filepaths, 'commit message'); ``` Key modules: @@ -515,12 +515,12 @@ cloning, detects private repositories, and handles PAT authentication. Simple in-memory cache with TTL (`cache/cache.ts`): ```typescript -import { cache } from "$cache/cache.ts"; +import { cache } from '$cache/cache.ts'; -cache.set("key", data, 300); // TTL in seconds -const value = cache.get("key"); // Returns undefined if expired -cache.delete("key"); -cache.deleteByPrefix("library:"); // Clear related entries +cache.set('key', data, 300); // TTL in seconds +const value = cache.get('key'); // Returns undefined if expired +cache.delete('key'); +cache.deleteByPrefix('library:'); // Clear related entries cache.clear(); ``` @@ -533,10 +533,10 @@ Optional AI integration (`ai/client.ts`) for generating commit messages from diffs. Supports OpenAI-compatible APIs including local models. ```typescript -import { isAIEnabled, generateCommitMessage } from "$utils/ai/client.ts"; +import { isAIEnabled, generateCommitMessage } from '$utils/ai/client.ts'; if (isAIEnabled()) { - const message = await generateCommitMessage(diffText); + const message = await generateCommitMessage(diffText); } ``` @@ -579,13 +579,13 @@ incorrect display. Use the shared date utilities in `src/lib/shared/dates.ts`: ```typescript -import { parseUTC, toUTC } from "$shared/dates"; +import { parseUTC, toUTC } from '$shared/dates'; // Parse SQLite timestamp to Date object (correctly interpreted as UTC) -const date = parseUTC("2026-01-17 03:21:52"); // Date in UTC +const date = parseUTC('2026-01-17 03:21:52'); // Date in UTC // Normalize to ISO 8601 string with Z suffix -const iso = toUTC("2026-01-17 03:21:52"); // "2026-01-17T03:21:52Z" +const iso = toUTC('2026-01-17 03:21:52'); // "2026-01-17T03:21:52Z" ``` **Never** manually append `'Z'` or manipulate timestamp strings directly. Always @@ -659,16 +659,16 @@ Every PCD requires a `pcd.json` manifest: ```json { - "name": "my-database", - "version": "1.0.0", - "description": "Custom Arr configurations", - "dependencies": { - "https://github.com/Dictionarry-Hub/schema": "main" - }, - "arr_types": ["radarr", "sonarr"], - "profilarr": { - "minimum_version": "2.0.0" - } + "name": "my-database", + "version": "1.0.0", + "description": "Custom Arr configurations", + "dependencies": { + "https://github.com/Dictionarry-Hub/schema": "main" + }, + "arr_types": ["radarr", "sonarr"], + "profilarr": { + "minimum_version": "2.0.0" + } } ``` @@ -706,15 +706,15 @@ changes appear immediately. ```typescript // Example: writer converts this Kysely query to a .sql file await writeOperation({ - databaseId: 1, - layer: "user", - description: "update-profile-score", - queries: [compiledKyselyQuery], - metadata: { - operation: "update", - entity: "quality_profile", - name: "HD Bluray + WEB", - }, + databaseId: 1, + layer: 'user', + description: 'update-profile-score', + queries: [compiledKyselyQuery], + metadata: { + operation: 'update', + entity: 'quality_profile', + name: 'HD Bluray + WEB' + } }); ``` @@ -813,13 +813,13 @@ name, description, schedule (cron expression), and handler function: ```typescript export const myJob: JobDefinition = { - name: "my_job", - description: "Does something useful", - schedule: "0 * * * *", // Every hour - handler: async (): Promise => { - // Job logic here - return { success: true, output: "Done" }; - }, + name: 'my_job', + description: 'Does something useful', + schedule: '0 * * * *', // Every hour + handler: async (): Promise => { + // Job logic here + return { success: true, output: 'Done' }; + } }; ``` @@ -865,60 +865,58 @@ don't interrupt the calling code. For simple notifications, use the builder directly: ```typescript -import { notify } from "$notifications/builder.ts"; +import { notify } from '$notifications/builder.ts'; -await notify("pcd.sync_success") - .generic("Sync Complete", "Synced 5 profiles to Radarr") - .send(); +await notify('pcd.sync_success').generic('Sync Complete', 'Synced 5 profiles to Radarr').send(); ``` For rich Discord notifications with embeds: ```typescript -import { notify, createEmbed, Colors } from "$notifications/builder.ts"; +import { notify, createEmbed, Colors } from '$notifications/builder.ts'; -await notify("rename.success") - .generic("Rename Complete", "Renamed 5 files") - .discord((d) => - d.embed( - createEmbed() - .title("Rename Complete") - .description("All files renamed successfully") - .field("Files", "5/5", true) - .field("Mode", "Live", true) - .color(Colors.SUCCESS) - .timestamp() - ) - ) - .send(); +await notify('rename.success') + .generic('Rename Complete', 'Renamed 5 files') + .discord((d) => + d.embed( + createEmbed() + .title('Rename Complete') + .description('All files renamed successfully') + .field('Files', '5/5', true) + .field('Mode', 'Live', true) + .color(Colors.SUCCESS) + .timestamp() + ) + ) + .send(); ``` For complex notifications, create a definition in `definitions/`: ```typescript // definitions/myFeature.ts -import { notify, createEmbed, Colors } from "../builder.ts"; +import { notify, createEmbed, Colors } from '../builder.ts'; interface MyNotificationParams { - log: MyJobLog; - config: { username?: string }; + log: MyJobLog; + config: { username?: string }; } export const myFeature = ({ log, config }: MyNotificationParams) => - notify(`myfeature.${log.status}`) - .generic("Feature Complete", `Processed ${log.count} items`) - .discord((d) => - d.embed( - createEmbed() - .title("Feature Complete") - .field("Processed", String(log.count), true) - .color(log.status === "success" ? Colors.SUCCESS : Colors.ERROR) - .timestamp() - ) - ); + notify(`myfeature.${log.status}`) + .generic('Feature Complete', `Processed ${log.count} items`) + .discord((d) => + d.embed( + createEmbed() + .title('Feature Complete') + .field('Processed', String(log.count), true) + .color(log.status === 'success' ? Colors.SUCCESS : Colors.ERROR) + .timestamp() + ) + ); // Usage: -import { notifications } from "$notifications/definitions/index.ts"; +import { notifications } from '$notifications/definitions/index.ts'; await notifications.myFeature({ log, config }).send(); ``` @@ -928,15 +926,15 @@ The embed builder (`notifiers/discord/embed.ts`) provides a fluent API: ```typescript createEmbed() - .author("Profilarr", iconUrl) - .title("Title") - .description("Description text") - .field("Name", "Value", true) // inline field - .fieldIf(condition, "Name", "Value") // conditional field - .color(Colors.SUCCESS) - .timestamp() - .footer("Profilarr") - .build(); + .author('Profilarr', iconUrl) + .title('Title') + .description('Description text') + .field('Name', 'Value', true) // inline field + .fieldIf(condition, 'Name', 'Value') // conditional field + .color(Colors.SUCCESS) + .timestamp() + .footer('Profilarr') + .build(); ``` Available colors: `Colors.SUCCESS`, `Colors.ERROR`, `Colors.WARNING`, @@ -1168,14 +1166,14 @@ list: tags: - Databases responses: - "200": + '200': description: List of databases content: application/json: schema: type: array items: - $ref: "../schemas/database.yaml#/Database" + $ref: '../schemas/database.yaml#/Database' ``` 2. Reference it in `openapi.yaml`: @@ -1183,7 +1181,7 @@ list: ```yaml paths: /databases: - $ref: "./paths/databases.yaml#/list" + $ref: './paths/databases.yaml#/list' ``` 3. Add any new schemas to `schemas/`: diff --git a/src/app.css b/src/app.css index 4786572..c5b0581 100644 --- a/src/app.css +++ b/src/app.css @@ -394,7 +394,7 @@ } ::-webkit-scrollbar-thumb { - @apply bg-neutral-300 dark:bg-neutral-700 rounded; + @apply rounded bg-neutral-300 dark:bg-neutral-700; } ::-webkit-scrollbar-thumb:hover { diff --git a/src/deno.d.ts b/src/deno.d.ts index 3950ddc..1240bb0 100644 --- a/src/deno.d.ts +++ b/src/deno.d.ts @@ -5,36 +5,5 @@ /** App version injected at build time */ declare const __APP_VERSION__: string; -declare namespace Deno { - export const env: { - get(key: string): string | undefined; - }; - - export function mkdir(path: string, options?: { recursive?: boolean }): Promise; - - export function writeTextFile( - path: string, - data: string, - options?: { append?: boolean } - ): Promise; - - export interface HttpClient { - close(): void; - } - - export interface CreateHttpClientOptions { - poolMaxIdlePerHost?: number; - poolIdleTimeout?: number | false; - } - - export function createHttpClient(options?: CreateHttpClientOptions): HttpClient; - - export interface DirEntry { - name: string; - isFile: boolean; - isDirectory: boolean; - isSymlink: boolean; - } - - export function readDir(path: string): AsyncIterable; -} +// Note: Deno namespace types are provided by Deno's built-in lib.deno.ns.d.ts +// Do not redeclare them here to avoid conflicts diff --git a/src/lib/api/v1.d.ts b/src/lib/api/v1.d.ts index b79df29..6dad1f0 100644 --- a/src/lib/api/v1.d.ts +++ b/src/lib/api/v1.d.ts @@ -4,532 +4,534 @@ */ export interface paths { - "/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Health check - * @description Returns the health status of the application and its components. - * - * Status values: - * - `healthy`: All components functioning normally - * - `degraded`: Core functionality works but some components have issues - * - `unhealthy`: Core functionality is broken - */ - get: operations["getHealth"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/openapi.json": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * OpenAPI specification - * @description Returns the OpenAPI specification for this API - */ - get: operations["getOpenApiSpec"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/entity-testing/evaluate": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Evaluate releases against custom formats - * @description Parses release titles and evaluates them against all custom formats in the specified database. - * - * This endpoint: - * - Parses each release title to extract metadata (resolution, source, languages, etc.) - * - Matches regex patterns using .NET-compatible regex via the parser service - * - Evaluates each release against all custom formats in the database - * - Returns which custom formats match each release - * - * Results are cached for performance - repeated requests with the same titles will be faster. - */ - post: operations["evaluateReleases"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/arr/library": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Arr instance library - * @description Fetches the movie or series library from an Arr instance. - * - * Returns a simplified list suitable for selection/matching. - * - For Radarr: Returns movies with id, title, year, and tmdbId - * - For Sonarr: Returns series with id, title, year, tvdbId, and available seasons - */ - get: operations["getLibrary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/arr/releases": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Search for releases - * @description Triggers an interactive search on an Arr instance and returns grouped/deduplicated results. - * - * For Radarr: Searches for releases for the specified movie. - * For Sonarr: Searches for season pack releases for the specified series and season. - * - * Results are grouped by title, combining information from multiple indexers. - */ - get: operations["getReleases"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; + '/health': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Returns the health status of the application and its components. + * + * Status values: + * - `healthy`: All components functioning normally + * - `degraded`: Core functionality works but some components have issues + * - `unhealthy`: Core functionality is broken + */ + get: operations['getHealth']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/openapi.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * OpenAPI specification + * @description Returns the OpenAPI specification for this API + */ + get: operations['getOpenApiSpec']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/entity-testing/evaluate': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Evaluate releases against custom formats + * @description Parses release titles and evaluates them against all custom formats in the specified database. + * + * This endpoint: + * - Parses each release title to extract metadata (resolution, source, languages, etc.) + * - Matches regex patterns using .NET-compatible regex via the parser service + * - Evaluates each release against all custom formats in the database + * - Returns which custom formats match each release + * + * Results are cached for performance - repeated requests with the same titles will be faster. + */ + post: operations['evaluateReleases']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/arr/library': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Arr instance library + * @description Fetches the movie or series library from an Arr instance. + * + * Returns a simplified list suitable for selection/matching. + * - For Radarr: Returns movies with id, title, year, and tmdbId + * - For Sonarr: Returns series with id, title, year, tvdbId, and available seasons + */ + get: operations['getLibrary']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/arr/releases': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Search for releases + * @description Triggers an interactive search on an Arr instance and returns grouped/deduplicated results. + * + * For Radarr: Searches for releases for the specified movie. + * For Sonarr: Searches for season pack releases for the specified series and season. + * + * Results are grouped by title, combining information from multiple indexers. + */ + get: operations['getReleases']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - /** - * @description Individual component health status - * @enum {string} - */ - ComponentStatus: "healthy" | "degraded" | "unhealthy"; - /** - * @description Overall health status - * @enum {string} - */ - HealthStatus: "healthy" | "degraded" | "unhealthy"; - HealthResponse: { - status: components["schemas"]["HealthStatus"]; - /** - * Format: date-time - * @description Current server time - */ - timestamp: string; - /** @description Application version */ - version: string; - /** @description Server uptime in seconds */ - uptime: number; - components: { - database: components["schemas"]["DatabaseHealth"]; - databases: components["schemas"]["DatabasesHealth"]; - jobs: components["schemas"]["JobsHealth"]; - backups: components["schemas"]["BackupsHealth"]; - logs: components["schemas"]["LogsHealth"]; - }; - }; - /** - * @description Type of media - * @enum {string} - */ - MediaType: "movie" | "series"; - ParsedInfo: { - /** @description Detected source (e.g., bluray, webdl, webrip) */ - source: string; - /** @description Detected resolution (e.g., 1080p, 2160p) */ - resolution: string; - /** @description Quality modifier (e.g., remux, none) */ - modifier: string; - /** @description Detected languages */ - languages: string[]; - /** @description Detected release group */ - releaseGroup?: string | null; - /** @description Detected year */ - year: number; - /** @description Detected edition (e.g., Director's Cut) */ - edition?: string | null; - /** @description Release type for series (single_episode, season_pack, etc.) */ - releaseType?: string | null; - }; - ReleaseInput: { - /** @description Release ID */ - id: number; - /** @description Release title to parse and evaluate */ - title: string; - type: components["schemas"]["MediaType"]; - }; - ReleaseEvaluation: { - /** @description Release ID */ - releaseId: number; - /** @description Release title */ - title: string; - /** @description Parsed release info (null if parsing failed) */ - parsed?: components["schemas"]["ParsedInfo"]; - /** @description Map of custom format ID to whether it matches */ - cfMatches: { - [key: string]: boolean; - }; - }; - EvaluateRequest: { - /** @description Database ID to use for custom format evaluation */ - databaseId: number; - /** @description Releases to evaluate */ - releases: components["schemas"]["ReleaseInput"][]; - }; - EvaluateResponse: { - /** @description Whether the parser service is available */ - parserAvailable: boolean; - /** @description Evaluation results for each release */ - evaluations: components["schemas"]["ReleaseEvaluation"][]; - }; - /** - * @description Type of Arr instance - * @enum {string} - */ - ArrType: "radarr" | "sonarr"; - LibraryMovieItem: { - /** @description Radarr movie ID */ - id: number; - /** @description Movie title */ - title: string; - /** @description Release year */ - year: number; - /** @description TMDB ID */ - tmdbId: number; - }; - LibrarySeriesItem: { - /** @description Sonarr series ID */ - id: number; - /** @description Series title */ - title: string; - /** @description First air year */ - year: number; - /** @description TVDB ID */ - tvdbId: number; - /** @description Available season numbers (excludes specials) */ - seasons: number[]; - }; - /** @description Library response varies by instance type */ - LibraryResponse: components["schemas"]["LibraryRadarrResponse"] | components["schemas"]["LibrarySonarrResponse"]; - GroupedRelease: { - /** @description Release title */ - title: string; - /** @description Release size in bytes */ - size: number; - /** @description Languages detected in the release */ - languages: string[]; - /** @description Indexers where this release was found */ - indexers: string[]; - /** @description Release flags (e.g., freeleech, internal) */ - flags: string[]; - }; - ReleasesResponse: { - type: components["schemas"]["ArrType"]; - /** @description Total number of raw releases before grouping */ - rawCount: number; - /** @description Grouped and deduplicated releases */ - releases: components["schemas"]["GroupedRelease"][]; - }; - ErrorResponse: { - /** @description Error message */ - error: string; - }; - DatabaseHealth: { - status: components["schemas"]["ComponentStatus"]; - /** @description Database query response time in milliseconds */ - responseTimeMs: number; - /** @description Error message if unhealthy */ - message?: string; - }; - DatabasesHealth: { - status: components["schemas"]["ComponentStatus"]; - /** @description Total number of PCD databases configured */ - total: number; - /** @description Number of enabled databases */ - enabled: number; - /** @description Number of databases with compiled cache */ - cached: number; - /** @description Number of disabled databases (compilation errors) */ - disabled: number; - /** @description Additional status information */ - message?: string; - }; - JobsHealth: { - status: components["schemas"]["ComponentStatus"]; - /** @description Last run time for each job */ - lastRun?: { - [key: string]: string | null; - }; - /** @description Additional status information */ - message?: string; - }; - BackupsHealth: { - status: components["schemas"]["ComponentStatus"]; - /** @description Whether backups are enabled */ - enabled: boolean; - /** - * Format: date-time - * @description Timestamp of last backup - */ - lastBackup?: string | null; - /** @description Number of backup files */ - count?: number; - /** @description Total size of all backups in bytes */ - totalSizeBytes?: number; - /** @description Configured retention period in days */ - retentionDays?: number; - /** @description Additional status information */ - message?: string; - }; - LogsHealth: { - status: components["schemas"]["ComponentStatus"]; - /** @description Total size of log files in bytes */ - totalSizeBytes?: number; - /** @description Number of log files */ - fileCount?: number; - /** - * Format: date - * @description Date of oldest log file - */ - oldestLog?: string | null; - /** - * Format: date - * @description Date of newest log file - */ - newestLog?: string | null; - /** @description Additional status information */ - message?: string; - }; - LibraryRadarrResponse: { - /** @enum {string} */ - type: "radarr"; - items: components["schemas"]["LibraryMovieItem"][]; - }; - LibrarySonarrResponse: { - /** @enum {string} */ - type: "sonarr"; - items: components["schemas"]["LibrarySeriesItem"][]; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * @description Individual component health status + * @enum {string} + */ + ComponentStatus: 'healthy' | 'degraded' | 'unhealthy'; + /** + * @description Overall health status + * @enum {string} + */ + HealthStatus: 'healthy' | 'degraded' | 'unhealthy'; + HealthResponse: { + status: components['schemas']['HealthStatus']; + /** + * Format: date-time + * @description Current server time + */ + timestamp: string; + /** @description Application version */ + version: string; + /** @description Server uptime in seconds */ + uptime: number; + components: { + database: components['schemas']['DatabaseHealth']; + databases: components['schemas']['DatabasesHealth']; + jobs: components['schemas']['JobsHealth']; + backups: components['schemas']['BackupsHealth']; + logs: components['schemas']['LogsHealth']; + }; + }; + /** + * @description Type of media + * @enum {string} + */ + MediaType: 'movie' | 'series'; + ParsedInfo: { + /** @description Detected source (e.g., bluray, webdl, webrip) */ + source: string; + /** @description Detected resolution (e.g., 1080p, 2160p) */ + resolution: string; + /** @description Quality modifier (e.g., remux, none) */ + modifier: string; + /** @description Detected languages */ + languages: string[]; + /** @description Detected release group */ + releaseGroup?: string | null; + /** @description Detected year */ + year: number; + /** @description Detected edition (e.g., Director's Cut) */ + edition?: string | null; + /** @description Release type for series (single_episode, season_pack, etc.) */ + releaseType?: string | null; + }; + ReleaseInput: { + /** @description Release ID */ + id: number; + /** @description Release title to parse and evaluate */ + title: string; + type: components['schemas']['MediaType']; + }; + ReleaseEvaluation: { + /** @description Release ID */ + releaseId: number; + /** @description Release title */ + title: string; + /** @description Parsed release info (null if parsing failed) */ + parsed?: components['schemas']['ParsedInfo']; + /** @description Map of custom format ID to whether it matches */ + cfMatches: { + [key: string]: boolean; + }; + }; + EvaluateRequest: { + /** @description Database ID to use for custom format evaluation */ + databaseId: number; + /** @description Releases to evaluate */ + releases: components['schemas']['ReleaseInput'][]; + }; + EvaluateResponse: { + /** @description Whether the parser service is available */ + parserAvailable: boolean; + /** @description Evaluation results for each release */ + evaluations: components['schemas']['ReleaseEvaluation'][]; + }; + /** + * @description Type of Arr instance + * @enum {string} + */ + ArrType: 'radarr' | 'sonarr'; + LibraryMovieItem: { + /** @description Radarr movie ID */ + id: number; + /** @description Movie title */ + title: string; + /** @description Release year */ + year: number; + /** @description TMDB ID */ + tmdbId: number; + }; + LibrarySeriesItem: { + /** @description Sonarr series ID */ + id: number; + /** @description Series title */ + title: string; + /** @description First air year */ + year: number; + /** @description TVDB ID */ + tvdbId: number; + /** @description Available season numbers (excludes specials) */ + seasons: number[]; + }; + /** @description Library response varies by instance type */ + LibraryResponse: + | components['schemas']['LibraryRadarrResponse'] + | components['schemas']['LibrarySonarrResponse']; + GroupedRelease: { + /** @description Release title */ + title: string; + /** @description Release size in bytes */ + size: number; + /** @description Languages detected in the release */ + languages: string[]; + /** @description Indexers where this release was found */ + indexers: string[]; + /** @description Release flags (e.g., freeleech, internal) */ + flags: string[]; + }; + ReleasesResponse: { + type: components['schemas']['ArrType']; + /** @description Total number of raw releases before grouping */ + rawCount: number; + /** @description Grouped and deduplicated releases */ + releases: components['schemas']['GroupedRelease'][]; + }; + ErrorResponse: { + /** @description Error message */ + error: string; + }; + DatabaseHealth: { + status: components['schemas']['ComponentStatus']; + /** @description Database query response time in milliseconds */ + responseTimeMs: number; + /** @description Error message if unhealthy */ + message?: string; + }; + DatabasesHealth: { + status: components['schemas']['ComponentStatus']; + /** @description Total number of PCD databases configured */ + total: number; + /** @description Number of enabled databases */ + enabled: number; + /** @description Number of databases with compiled cache */ + cached: number; + /** @description Number of disabled databases (compilation errors) */ + disabled: number; + /** @description Additional status information */ + message?: string; + }; + JobsHealth: { + status: components['schemas']['ComponentStatus']; + /** @description Last run time for each job */ + lastRun?: { + [key: string]: string | null; + }; + /** @description Additional status information */ + message?: string; + }; + BackupsHealth: { + status: components['schemas']['ComponentStatus']; + /** @description Whether backups are enabled */ + enabled: boolean; + /** + * Format: date-time + * @description Timestamp of last backup + */ + lastBackup?: string | null; + /** @description Number of backup files */ + count?: number; + /** @description Total size of all backups in bytes */ + totalSizeBytes?: number; + /** @description Configured retention period in days */ + retentionDays?: number; + /** @description Additional status information */ + message?: string; + }; + LogsHealth: { + status: components['schemas']['ComponentStatus']; + /** @description Total size of log files in bytes */ + totalSizeBytes?: number; + /** @description Number of log files */ + fileCount?: number; + /** + * Format: date + * @description Date of oldest log file + */ + oldestLog?: string | null; + /** + * Format: date + * @description Date of newest log file + */ + newestLog?: string | null; + /** @description Additional status information */ + message?: string; + }; + LibraryRadarrResponse: { + /** @enum {string} */ + type: 'radarr'; + items: components['schemas']['LibraryMovieItem'][]; + }; + LibrarySonarrResponse: { + /** @enum {string} */ + type: 'sonarr'; + items: components['schemas']['LibrarySeriesItem'][]; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - getHealth: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Health check response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HealthResponse"]; - }; - }; - }; - }; - getOpenApiSpec: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OpenAPI specification */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - }; - }; - evaluateReleases: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["EvaluateRequest"]; - }; - }; - responses: { - /** @description Evaluation results */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluateResponse"]; - }; - }; - /** @description Invalid request (missing databaseId or releases) */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Database cache not available */ - 500: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getLibrary: { - parameters: { - query: { - /** @description Arr instance ID */ - instanceId: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Library items */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["LibraryResponse"]; - }; - }; - /** @description Invalid or missing instanceId */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Instance not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Failed to fetch library */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; - getReleases: { - parameters: { - query: { - /** @description Arr instance ID */ - instanceId: number; - /** @description Movie ID (Radarr) or Series ID (Sonarr) */ - itemId: number; - /** @description Season number for Sonarr searches (defaults to 1) */ - season?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Release search results */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ReleasesResponse"]; - }; - }; - /** @description Invalid or missing parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Instance not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - /** @description Failed to fetch releases */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorResponse"]; - }; - }; - }; - }; + getHealth: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Health check response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HealthResponse']; + }; + }; + }; + }; + getOpenApiSpec: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OpenAPI specification */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': Record; + }; + }; + }; + }; + evaluateReleases: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EvaluateRequest']; + }; + }; + responses: { + /** @description Evaluation results */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluateResponse']; + }; + }; + /** @description Invalid request (missing databaseId or releases) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Database cache not available */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getLibrary: { + parameters: { + query: { + /** @description Arr instance ID */ + instanceId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Library items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LibraryResponse']; + }; + }; + /** @description Invalid or missing instanceId */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Instance not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Failed to fetch library */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + getReleases: { + parameters: { + query: { + /** @description Arr instance ID */ + instanceId: number; + /** @description Movie ID (Radarr) or Series ID (Sonarr) */ + itemId: number; + /** @description Season number for Sonarr searches (defaults to 1) */ + season?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Release search results */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ReleasesResponse']; + }; + }; + /** @description Invalid or missing parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Instance not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Failed to fetch releases */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; } diff --git a/src/lib/client/stores/accent.ts b/src/lib/client/stores/accent.ts index d77fcf4..7ae77ed 100644 --- a/src/lib/client/stores/accent.ts +++ b/src/lib/client/stores/accent.ts @@ -8,37 +8,112 @@ import { browser } from '$app/environment'; export type AccentColor = 'blue' | 'yellow' | 'green' | 'orange' | 'teal' | 'purple' | 'rose'; // Color palettes for each accent (matching Tailwind shades) -const colorPalettes: Record = { +const colorPalettes: Record< + AccentColor, + { + 50: string; + 100: string; + 200: string; + 300: string; + 400: string; + 500: string; + 600: string; + 700: string; + 800: string; + 900: string; + 950: string; + } +> = { blue: { - 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', - 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554' }, yellow: { - 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047', 400: '#facc15', - 500: '#eab308', 600: '#ca8a04', 700: '#a16207', 800: '#854d0e', 900: '#713f12', 950: '#422006' + 50: '#fefce8', + 100: '#fef9c3', + 200: '#fef08a', + 300: '#fde047', + 400: '#facc15', + 500: '#eab308', + 600: '#ca8a04', + 700: '#a16207', + 800: '#854d0e', + 900: '#713f12', + 950: '#422006' }, green: { - 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', - 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + 800: '#166534', + 900: '#14532d', + 950: '#052e16' }, orange: { - 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', - 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' + 50: '#fff7ed', + 100: '#ffedd5', + 200: '#fed7aa', + 300: '#fdba74', + 400: '#fb923c', + 500: '#f97316', + 600: '#ea580c', + 700: '#c2410c', + 800: '#9a3412', + 900: '#7c2d12', + 950: '#431407' }, teal: { - 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', - 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' + 50: '#f0fdfa', + 100: '#ccfbf1', + 200: '#99f6e4', + 300: '#5eead4', + 400: '#2dd4bf', + 500: '#14b8a6', + 600: '#0d9488', + 700: '#0f766e', + 800: '#115e59', + 900: '#134e4a', + 950: '#042f2e' }, purple: { - 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', - 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' + 50: '#faf5ff', + 100: '#f3e8ff', + 200: '#e9d5ff', + 300: '#d8b4fe', + 400: '#c084fc', + 500: '#a855f7', + 600: '#9333ea', + 700: '#7e22ce', + 800: '#6b21a8', + 900: '#581c87', + 950: '#3b0764' }, rose: { - 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', - 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' + 50: '#fff1f2', + 100: '#ffe4e6', + 200: '#fecdd3', + 300: '#fda4af', + 400: '#fb7185', + 500: '#f43f5e', + 600: '#e11d48', + 700: '#be123c', + 800: '#9f1239', + 900: '#881337', + 950: '#4c0519' } }; diff --git a/src/lib/client/stores/dirty.ts b/src/lib/client/stores/dirty.ts index 8f9b048..3074ac1 100644 --- a/src/lib/client/stores/dirty.ts +++ b/src/lib/client/stores/dirty.ts @@ -91,11 +91,13 @@ export function resetFromServer(newServerData: T) { /** * Clear all state (call on unmount/navigation away) + * Sets both stores to same empty object so isDirty = false */ export function clear() { isNewMode.set(false); - originalSnapshot.set(null); - currentData.set({}); + const empty = {}; + originalSnapshot.set(empty); + currentData.set(empty); } /** diff --git a/src/lib/client/ui/actions/ActionButton.svelte b/src/lib/client/ui/actions/ActionButton.svelte index 75d2c3f..8bb4e80 100644 --- a/src/lib/client/ui/actions/ActionButton.svelte +++ b/src/lib/client/ui/actions/ActionButton.svelte @@ -52,7 +52,11 @@ on:click > {#if icon} - + {/if} diff --git a/src/lib/client/ui/actions/SearchAction.svelte b/src/lib/client/ui/actions/SearchAction.svelte index 62f8880..5fc2299 100644 --- a/src/lib/client/ui/actions/SearchAction.svelte +++ b/src/lib/client/ui/actions/SearchAction.svelte @@ -66,7 +66,9 @@ on:focus={() => (isFocused = true)} on:blur={() => (isFocused = false)} placeholder={activeQuery ? '' : placeholder} - class="h-full w-full bg-transparent pr-10 text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400 {activeQuery ? 'pl-2' : 'pl-10'}" + class="h-full w-full bg-transparent pr-10 text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400 {activeQuery + ? 'pl-2' + : 'pl-10'}" /> diff --git a/src/lib/client/ui/badge/Badge.svelte b/src/lib/client/ui/badge/Badge.svelte index 8eca1e1..d11ae56 100644 --- a/src/lib/client/ui/badge/Badge.svelte +++ b/src/lib/client/ui/badge/Badge.svelte @@ -24,7 +24,9 @@ {#if icon} diff --git a/src/lib/client/ui/button/Button.svelte b/src/lib/client/ui/button/Button.svelte index 48c03b7..d1532b1 100644 --- a/src/lib/client/ui/button/Button.svelte +++ b/src/lib/client/ui/button/Button.svelte @@ -24,25 +24,19 @@ 'bg-accent-600 text-white hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600', secondary: 'border border-neutral-300 bg-white text-neutral-700 hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700', - danger: - 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600', + danger: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600', ghost: 'border border-neutral-300 bg-white text-neutral-700 hover:bg-neutral-50 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700' }; - $: baseTextColor = textColor || (variant === 'ghost' ? 'text-neutral-700 dark:text-neutral-300' : ''); - $: baseIconColor = iconColor || (variant === 'ghost' ? 'text-neutral-500 dark:text-neutral-400' : ''); + $: baseTextColor = + textColor || (variant === 'ghost' ? 'text-neutral-700 dark:text-neutral-300' : ''); + $: baseIconColor = + iconColor || (variant === 'ghost' ? 'text-neutral-500 dark:text-neutral-400' : ''); $: classes = `${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]}`; - {#if open} diff --git a/src/lib/client/ui/navigation/navbar/navbar.svelte b/src/lib/client/ui/navigation/navbar/navbar.svelte index ed114af..3cee6c7 100644 --- a/src/lib/client/ui/navigation/navbar/navbar.svelte +++ b/src/lib/client/ui/navigation/navbar/navbar.svelte @@ -7,12 +7,12 @@