diff --git a/.gitignore b/.gitignore index 96370a4674..15980d87e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .dev/dependencies .dev/coverage .dev/artifacts +.dev/db-studio */.dev/artifacts */.dev/coverage */.dev/dependencies diff --git a/packages/cli/build.gradle.kts b/packages/cli/build.gradle.kts index 9faa579817..640511af66 100644 --- a/packages/cli/build.gradle.kts +++ b/packages/cli/build.gradle.kts @@ -2414,7 +2414,7 @@ tasks { val allSamples = layout.projectDirectory.dir("src/projects") .asFile .listFiles() - .filter { it.isDirectory() } + .filter { it.isDirectory() && it.name != "db-studio" } .map { it.toPath() to it.name } val builtSamples = layout.buildDirectory.dir("packed-samples") @@ -2436,10 +2436,30 @@ tasks { dependsOn(allSamplePackTasks) } + val prepareDbStudioResources by registering(Copy::class) { + group = "build" + description = "Prepare Database Studio resources for embedding in CLI" + + from(layout.projectDirectory.dir("src/db-studio/api")) { + into("api") + exclude("config.ts") // Generated at runtime with injected config + exclude(".dev/**") // Exclude development-only dependencies directory + exclude("node_modules/**") // Dependencies installed at runtime + } + + // Copy built UI (dist/ folder only, not source or node_modules) + from(layout.projectDirectory.dir("src/db-studio/ui/dist")) { + into("ui") + } + + into(layout.buildDirectory.dir("resources/main/META-INF/elide/db-studio")) + } + processResources { dependsOn( ":packages:graalvm:buildRustNativesForHostDebug", prepKotlinResources, + prepareDbStudioResources, packSamples, allSamplePackTasks, ) diff --git a/packages/cli/src/db-studio/README.md b/packages/cli/src/db-studio/README.md new file mode 100644 index 0000000000..4df8dc2075 --- /dev/null +++ b/packages/cli/src/db-studio/README.md @@ -0,0 +1,44 @@ +# Database Studio + +Web-based database management UI that runs on the Elide runtime. At the moment it only supports SQLite with Elide's `elide:sqlite` js bindings. We assume that an existing valid database file exists for the `elide db studio` command to find. If you need a database file to run this on, [Northwind](https://github.com/jpwhite3/northwind-SQLite3) and [Chinook](https://github.com/lerocha/chinook-database/releases) are some good prepopulated examples. + +## Directory Structure + +``` +db-studio/ +├── api/ # TypeScript REST API server +│ ├── ... +│ ├── index.ts # Server entrypoint +│ └── elide.pkl +└── ui/ # React frontend application + ├── src/ + └── dist/ # Vite production build +``` + +## Running Locally + +### Using the Gradle Command + +The best way to run this locally is through the CLI in JVM mode. Simply passing `db studio` as args. + +**Note:** In order to see UI changes you've made, you'll need to run `pnpm build` in the `ui/` directory first. + +```bash +# Build the UI +cd packages/cli/src/db-studio/ui +pnpm build + +# Go back to the elide repo root +# Then run the db studio command +./gradlew :packages:cli:run --args="db studio" + +# With a specific database +./gradlew :packages:cli:run --args="db studio path/to/database.db" +``` + +### Running api or ui independently + +Database Studio consists of two separate projects that can be run independently: + +- `api`: run directly by calling `elide run index.ts` within the directory. +- `ui`: using `pnpm dev` within the directory. diff --git a/packages/cli/src/db-studio/api/.dev/dependencies/npm/package-lock.kdl b/packages/cli/src/db-studio/api/.dev/dependencies/npm/package-lock.kdl new file mode 120000 index 0000000000..a9e4ceefd3 --- /dev/null +++ b/packages/cli/src/db-studio/api/.dev/dependencies/npm/package-lock.kdl @@ -0,0 +1 @@ +/Users/francis/code/millpointlabs/elide/elide/packages/cli/src/db-studio/api/package-lock.kdl \ No newline at end of file diff --git a/packages/cli/src/db-studio/api/.dev/dependencies/npm/package.json b/packages/cli/src/db-studio/api/.dev/dependencies/npm/package.json new file mode 100644 index 0000000000..6c3badee97 --- /dev/null +++ b/packages/cli/src/db-studio/api/.dev/dependencies/npm/package.json @@ -0,0 +1,13 @@ +{ + "name": "db-studio-api", + "version": "1.0.0", + "description": "Database Studio API Server", + "main": "index.ts", + "scripts": {}, + "dependencies": { + "zod": "4" + }, + "devDependencies": { + "@elide-dev/types": "1.0.0-beta10" + } +} \ No newline at end of file diff --git a/packages/cli/src/db-studio/api/.dev/elide.lock.bin b/packages/cli/src/db-studio/api/.dev/elide.lock.bin new file mode 100644 index 0000000000..182f6907dc Binary files /dev/null and b/packages/cli/src/db-studio/api/.dev/elide.lock.bin differ diff --git a/packages/cli/src/db-studio/api/config.ts b/packages/cli/src/db-studio/api/config.ts new file mode 100644 index 0000000000..9203008216 --- /dev/null +++ b/packages/cli/src/db-studio/api/config.ts @@ -0,0 +1,22 @@ +import type { DiscoveredDatabase } from "./database.ts"; + +/** + * Database Studio Configuration + * + * This is a sample configuration file. In production, this would be + * generated by DbStudioCommand.kt based on discovered databases. + */ + +const config = { + port: 4984, + databases: [ + { + path: "./sample.db", + name: "sample.db", + size: 0, + lastModified: Date.now(), + } + ] as DiscoveredDatabase[], +}; + +export default config; diff --git a/packages/cli/src/db-studio/api/database.ts b/packages/cli/src/db-studio/api/database.ts new file mode 100644 index 0000000000..61d5d45a7a --- /dev/null +++ b/packages/cli/src/db-studio/api/database.ts @@ -0,0 +1,667 @@ +/** + * Database API Layer + * + * Provides abstraction over database operations for the DB Studio. + * This layer isolates all database-specific logic, making it easy to: + * - Swap SQLite for other databases (PostgreSQL, MySQL, etc.) + * - Add caching, connection pooling, or other optimizations + * - Centralize error handling and validation + */ + +import type { Database, Statement } from "elide:sqlite"; +import type { ColumnMetadata, ForeignKeyReference, Filter } from "./http/schemas.ts"; + +/** + * Log SQL queries to console + */ +export function logQuery(sql: string, params?: unknown[]): void { + const timestamp = new Date().toISOString(); + const paramsStr = params && params.length > 0 ? ` [${params.join(", ")}]` : ""; + console.log(`[${timestamp}] SQL: ${sql}${paramsStr}`); +} + +export interface DiscoveredDatabase { + id: string; + path: string; + name: string; + size: number; + lastModified: number; +} + +export type TableType = 'table' | 'view'; + +export interface TableInfo { + name: string; + type: TableType; + rowCount: number; +} + +export type TableData = { + name: string; + columns: ColumnMetadata[]; + rows: unknown[][]; + totalRows: number; + metadata: { + executionTimeMs: number; + sql: string; + rowCount: number; + }; +}; + +export interface DatabaseInfo { + path: string; + name: string; + size: number; + lastModified: number; + tableCount: number; +} + +interface TableNameRow { + name: string; + type: 'table' | 'view'; +} + +interface CountRow { + count: number; +} + +/** + * Get list of tables and views in a database + */ +export function getTables(db: Database): TableInfo[] { + // First query: get all table and view names with their types + // Exclude SQLite system tables (sqlite_sequence, sqlite_stat1, etc.) + const tableNamesQuery = "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name"; + const tablesQuery: Statement = db.query(tableNamesQuery); + const tables = tablesQuery.all(); + + if (tables.length === 0) { + return []; + } + + // Second query: get counts for all tables and views in a single UNION ALL query + const tableCountQuery = tables.map(({ name }) => + `SELECT '${name}' as tableName, COUNT(*) as count FROM "${name}"` + ).join(' UNION ALL '); + + interface CountResultRow { + tableName: string; + count: number; + } + + const countsQuery: Statement = db.query(tableCountQuery); + const counts = countsQuery.all(); + + // Create a map for O(1) lookup + const countMap = new Map(counts.map(({ tableName, count }) => [tableName, count])); + + return tables.map(({ name, type }) => ({ + name, + type, + rowCount: countMap.get(name) ?? 0, + })); +} + +interface PragmaTableInfoRow { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +} + +interface PragmaForeignKeyRow { + id: number; + seq: number; + table: string; + from: string; + to: string; + on_update: string; + on_delete: string; + match: string; +} + +interface PragmaIndexListRow { + seq: number; + name: string; + unique: number; + origin: string; + partial: number; +} + +interface PragmaIndexInfoRow { + seqno: number; + cid: number; + name: string; +} + +/** + * Get comprehensive column metadata for a table + */ +export function getColumnMetadata(db: Database, tableName: string): ColumnMetadata[] { + // Get basic column information + const tableInfoSql = `SELECT * FROM pragma_table_info('${tableName}')`; + const tableInfoQuery: Statement = db.prepare(tableInfoSql); + const columns = tableInfoQuery.all(); + + // Get foreign key information + const foreignKeySql = `SELECT * FROM pragma_foreign_key_list('${tableName}')`; + const foreignKeyQuery: Statement = db.prepare(foreignKeySql); + const foreignKeys = foreignKeyQuery.all(); + + // Build foreign key map: column name -> foreign key reference + const foreignKeyMap = new Map(); + for (const fk of foreignKeys) { + foreignKeyMap.set(fk.from, { + table: fk.table, + column: fk.to, + onUpdate: fk.on_update, + onDelete: fk.on_delete, + }); + } + + // Get unique constraint information from indexes + const indexListSql = `SELECT * FROM pragma_index_list('${tableName}')`; + const indexListQuery: Statement = db.prepare(indexListSql); + const indexes = indexListQuery.all(); + + // Build set of columns that have unique constraints + const uniqueColumns = new Set(); + for (const index of indexes) { + if (index.unique === 1) { + const indexInfoSql = `SELECT * FROM pragma_index_info('${index.name}')`; + const indexInfoQuery: Statement = db.prepare(indexInfoSql); + const indexInfo = indexInfoQuery.all(); + + // Only mark as unique if it's a single-column index + if (indexInfo.length === 1) { + const colName = indexInfo[0].name; + if (colName) { + uniqueColumns.add(colName); + } + } + } + } + + // Build the column metadata array + return columns.map((col): ColumnMetadata => { + const isAutoIncrement = col.pk === 1 && col.type.toUpperCase() === "INTEGER"; + + return { + name: col.name, + type: col.type, + nullable: col.notnull === 0, + primaryKey: col.pk > 0, + defaultValue: col.dflt_value, + foreignKey: foreignKeyMap.get(col.name), + unique: uniqueColumns.has(col.name), + autoIncrement: isAutoIncrement, + }; + }); +} + +/** + * Build WHERE clause and parameter values from filters + * Returns an object with the WHERE clause (without the WHERE keyword) and the parameter values + */ +function buildWhereClause( + filters: Filter[], + columns: ColumnMetadata[] +): { whereClause: string; params: unknown[] } { + if (filters.length === 0) { + return { whereClause: '', params: [] }; + } + + const columnNames = new Set(columns.map(col => col.name)); + const conditions: string[] = []; + const params: unknown[] = []; + + for (const filter of filters) { + // Validate column exists + if (!columnNames.has(filter.column)) { + throw new Error(`Invalid filter column: "${filter.column}" does not exist in table`); + } + + const quotedColumn = `"${filter.column}"`; + + switch (filter.operator) { + case 'eq': + conditions.push(`${quotedColumn} = ?`); + params.push(filter.value); + break; + case 'neq': + conditions.push(`${quotedColumn} <> ?`); + params.push(filter.value); + break; + case 'gt': + conditions.push(`${quotedColumn} > ?`); + params.push(filter.value); + break; + case 'gte': + conditions.push(`${quotedColumn} >= ?`); + params.push(filter.value); + break; + case 'lt': + conditions.push(`${quotedColumn} < ?`); + params.push(filter.value); + break; + case 'lte': + conditions.push(`${quotedColumn} <= ?`); + params.push(filter.value); + break; + case 'like': + conditions.push(`${quotedColumn} LIKE ?`); + params.push(filter.value); + break; + case 'not_like': + conditions.push(`${quotedColumn} NOT LIKE ?`); + params.push(filter.value); + break; + case 'in': + if (!Array.isArray(filter.value)) { + throw new Error(`Filter operator 'in' requires an array value for column "${filter.column}"`); + } + if (filter.value.length === 0) { + throw new Error(`Filter operator 'in' requires at least one value for column "${filter.column}"`); + } + const placeholders = filter.value.map(() => '?').join(', '); + conditions.push(`${quotedColumn} IN (${placeholders})`); + params.push(...filter.value); + break; + case 'is_null': + conditions.push(`${quotedColumn} IS NULL`); + break; + case 'is_not_null': + conditions.push(`${quotedColumn} IS NOT NULL`); + break; + default: + throw new Error(`Unsupported filter operator: ${filter.operator}`); + } + } + + return { + whereClause: conditions.join(' AND '), + params, + }; +} + +/** + * Get table data with schema and rows + * Supports optional sorting by column name and direction and filtering via WHERE clause + */ +export function getTableData( + db: Database, + tableName: string, + limit: number = 100, + offset: number = 0, + sortColumn: string | null = null, + sortDirection: 'asc' | 'desc' | null = null, + filters: Filter[] | null = null +): TableData { + const startTime = performance.now(); + + // Get column metadata + const columns = getColumnMetadata(db, tableName); + + // Validate sort column if provided + if (sortColumn && sortDirection) { + const columnExists = columns.some(col => col.name === sortColumn); + if (!columnExists) { + throw new Error(`Invalid sort column: "${sortColumn}" does not exist in table "${tableName}"`); + } + } + + // Build WHERE clause from filters + const { whereClause, params: whereParams } = filters && filters.length > 0 + ? buildWhereClause(filters, columns) + : { whereClause: '', params: [] }; + + // Build SQL query with optional WHERE and ORDER BY clauses + let dataSql = `SELECT * FROM "${tableName}"`; + + if (whereClause) { + dataSql += ` WHERE ${whereClause}`; + } + + if (sortColumn && sortDirection) { + // Column name is validated above, but we still quote it for safety + dataSql += ` ORDER BY "${sortColumn}" ${sortDirection.toUpperCase()}`; + } + + dataSql += ` LIMIT ${limit} OFFSET ${offset}`; + + logQuery(dataSql, whereParams); + const dataQuery = db.query(dataSql); + const rows = whereParams.length > 0 ? dataQuery.all(...(whereParams as (string | number | null)[])) : dataQuery.all(); + + // Get total row count with same WHERE clause (no logging - background query) + let countSql = `SELECT COUNT(*) as count FROM "${tableName}"`; + if (whereClause) { + countSql += ` WHERE ${whereClause}`; + } + const countQuery: Statement = db.query(countSql); + const countResult = whereParams.length > 0 ? countQuery.get(...(whereParams as (string | number | null)[])) : countQuery.get(); + const totalRows = countResult?.count ?? 0; + + const endTime = performance.now(); + + return { + name: tableName, + columns, + rows: rows.map((row: unknown) => columns.map(col => (row as Record)[col.name])), + totalRows, + metadata: { + executionTimeMs: Number((endTime - startTime).toFixed(2)), + sql: dataSql, + rowCount: rows.length, + }, + }; +} + +/** + * Get database metadata + */ +export function getDatabaseInfo(db: Database, dbPath: string): DatabaseInfo { + // Exclude SQLite system tables from count (no logging - background query) + const sql = "SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"; + const tablesQuery: Statement = db.query(sql); + const tablesResult = tablesQuery.get(); + + // Extract name from path + const pathParts = dbPath.split('/'); + const name = pathParts[pathParts.length - 1]; + + return { + path: dbPath, + name, + size: 0, // Will be populated by calling code if available + lastModified: 0, // Will be populated by calling code if available + tableCount: tablesResult?.count ?? 0, + }; +} + +/** + * Execute a raw SQL query and extract column metadata where possible + * For ad-hoc queries, we can't extract full metadata, so we provide basic info + */ +export function executeQuery( + db: Database, + sql: string +): { columns: ColumnMetadata[], rows: unknown[][], data: Record[] } { + logQuery(sql); + const query = db.query(sql); + const results = query.all(); + + if (results.length === 0) { + return { columns: [], rows: [], data: [] }; + } + + const firstRow = results[0] as Record; + const columnNames = Object.keys(firstRow); + + // For ad-hoc queries, we can only infer basic column info from the data + const columns: ColumnMetadata[] = columnNames.map(name => ({ + name, + type: inferColumnType(firstRow[name]), + nullable: true, // We can't know for sure + primaryKey: false, // We can't determine this for arbitrary queries + })); + + const rows = results.map((row: unknown) => + columnNames.map(col => (row as Record)[col]) + ); + + return { columns, rows, data: results as Record[] }; +} + +/** + * Infer a SQLite type from a JavaScript value + */ +function inferColumnType(value: unknown): string { + if (value === null || value === undefined) return "NULL"; + if (typeof value === "number") { + return Number.isInteger(value) ? "INTEGER" : "REAL"; + } + if (typeof value === "string") return "TEXT"; + if (typeof value === "boolean") return "INTEGER"; // SQLite stores booleans as integers + return "BLOB"; +} + +/** + * Validate that a database path is accessible + */ +export function validateDatabase(db: Database): boolean { + try { + // Try a simple query to verify the database is valid + const sql = "SELECT 1"; + const query = db.query(sql); + query.get(); + return true; + } catch (err) { + return false; + } +} + +/** + * Format a value for SQL display (shows actual values instead of placeholders) + */ +function formatSqlValue(value: unknown): string { + if (value === null) return 'NULL'; + if (value === undefined) return 'DEFAULT'; + if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; + if (typeof value === 'number' || typeof value === 'bigint') return String(value); + if (typeof value === 'boolean') return value ? '1' : '0'; + return `'${String(value)}'`; +} + +/** + * Build a human-readable SQL string with actual values (for error messages) + */ +function buildDisplaySql(sql: string, values: unknown[]): string { + let displaySql = sql; + for (const value of values) { + displaySql = displaySql.replace('?', formatSqlValue(value)); + } + return displaySql; +} + +/** + * Custom error class that includes SQL context + */ +export class SQLError extends Error { + public readonly sql: string; + + constructor(message: string, sql: string) { + super(message); + this.name = 'SQLError'; + this.sql = sql; + } +} + +/** + * Delete rows from a table based on primary key values + * @param db Database instance + * @param tableName Name of the table + * @param primaryKeys Array of primary key objects (e.g., [{ id: 1 }, { id: 2, name: "foo" }]) + * @returns Object with sql (the first DELETE statement for context) + * + * Note: rowsAffected is not currently available from the elide sqlite library + */ +export function deleteRows( + db: Database, + tableName: string, + primaryKeys: Record[] +): { sql?: string } { + if (primaryKeys.length === 0) { + return {}; + } + + // Get column metadata to identify primary key columns + const columns = getColumnMetadata(db, tableName); + const pkColumns = columns.filter(col => col.primaryKey); + + if (pkColumns.length === 0) { + throw new Error(`Table "${tableName}" has no primary key`); + } + + // Validate that all primary keys have the required columns + const pkColumnNames = pkColumns.map(col => col.name); + for (const pk of primaryKeys) { + for (const colName of pkColumnNames) { + if (!(colName in pk)) { + throw new Error(`Primary key missing required column: "${colName}"`); + } + } + } + + // Build display SQL for the first delete (for error context) + const firstPk = primaryKeys[0]; + const firstConditions = pkColumns.map(col => { + const value = firstPk[col.name]; + return value === null ? `"${col.name}" IS NULL` : `"${col.name}" = ?`; + }).join(' AND '); + const firstValues = pkColumns.map(col => firstPk[col.name]).filter(v => v !== null); + const firstSql = `DELETE FROM "${tableName}" WHERE ${firstConditions}`; + const displaySql = buildDisplaySql(firstSql, firstValues); + + try { + // Execute all delete operations in a transaction for atomicity + db.transaction(() => { + for (const pk of primaryKeys) { + // Handle null values with IS NULL instead of = ? + const conditions = pkColumns.map(col => { + const value = pk[col.name]; + return value === null ? `"${col.name}" IS NULL` : `"${col.name}" = ?`; + }).join(' AND '); + // Only include non-null values as parameters + const values = pkColumns.map(col => pk[col.name]).filter(v => v !== null); + const sql = `DELETE FROM "${tableName}" WHERE ${conditions}`; + + logQuery(sql, values); + const stmt = db.prepare(sql); + stmt.run(...(values as (string | number)[])); + } + })(); // Execute immediately + + return { sql: displaySql }; + } catch (err) { + // Re-throw with SQL context + const message = err instanceof Error ? err.message : String(err); + throw new SQLError(message, displaySql); + } +} + +/** + * Insert a new row into a table + * @param db Database instance + * @param tableName Name of the table to insert into + * @param row Object mapping column names to values (undefined values are omitted to use DEFAULT) + * @returns Object with sql + * + * Note: rowsAffected and lastInsertRowid are not currently available from the elide sqlite library + */ +export function insertRow( + db: Database, + tableName: string, + row: Record +): { sql: string } { + // Filter out undefined values (these will use DEFAULT) + const entries = Object.entries(row).filter(([_, value]) => value !== undefined); + + if (entries.length === 0) { + throw new Error("No columns specified for insert"); + } + + // Build INSERT query with parameterized values + const columnNames = entries.map(([key]) => `"${key}"`).join(', '); + const placeholders = entries.map(() => '?').join(', '); + const values = entries.map(([_, value]) => value); + + const sql = `INSERT INTO "${tableName}" (${columnNames}) VALUES (${placeholders})`; + const displaySql = buildDisplaySql(sql, values); + + logQuery(sql, values); + + try { + const stmt = db.prepare(sql); + stmt.run(...(values as (string | number | null)[])); + + return { + sql: displaySql, + }; + } catch (err) { + // Re-throw with SQL context + const message = err instanceof Error ? err.message : String(err); + throw new SQLError(message, displaySql); + } +} + +/** + * Update a row in a table based on primary key values + * @param db Database instance + * @param tableName Name of the table to update + * @param primaryKey Object mapping primary key column names to values + * @param updates Object mapping column names to new values + * @returns Object with sql + */ +export function updateRow( + db: Database, + tableName: string, + primaryKey: Record, + updates: Record +): { sql: string } { + // Get column metadata to identify primary key columns + const columns = getColumnMetadata(db, tableName); + const pkColumns = columns.filter(col => col.primaryKey); + + if (pkColumns.length === 0) { + throw new Error(`Table "${tableName}" has no primary key`); + } + + // Validate that primary key has the required columns + const pkColumnNames = pkColumns.map(col => col.name); + for (const colName of pkColumnNames) { + if (!(colName in primaryKey)) { + throw new Error(`Primary key missing required column: "${colName}"`); + } + } + + // Filter out undefined values from updates + const updateEntries = Object.entries(updates).filter(([_, value]) => value !== undefined); + + if (updateEntries.length === 0) { + throw new Error("No columns specified for update"); + } + + // Build SET clause + const setClause = updateEntries.map(([key]) => `"${key}" = ?`).join(', '); + const setValues = updateEntries.map(([_, value]) => value); + + // Build WHERE clause from primary key (handle null values with IS NULL) + const whereClause = pkColumns.map(col => { + const value = primaryKey[col.name]; + return value === null ? `"${col.name}" IS NULL` : `"${col.name}" = ?`; + }).join(' AND '); + // Only include non-null primary key values as parameters + const pkValues = pkColumns.map(col => primaryKey[col.name]).filter(v => v !== null); + + const allValues = [...setValues, ...pkValues]; + const sql = `UPDATE "${tableName}" SET ${setClause} WHERE ${whereClause}`; + const displaySql = buildDisplaySql(sql, allValues); + + logQuery(sql, allValues); + + try { + const stmt = db.prepare(sql); + stmt.run(...(allValues as (string | number | null)[])); + + return { + sql: displaySql, + }; + } catch (err) { + // Re-throw with SQL context + const message = err instanceof Error ? err.message : String(err); + throw new SQLError(message, displaySql); + } +} diff --git a/packages/cli/src/db-studio/api/elide.pkl b/packages/cli/src/db-studio/api/elide.pkl new file mode 100644 index 0000000000..fd21b1621b --- /dev/null +++ b/packages/cli/src/db-studio/api/elide.pkl @@ -0,0 +1,22 @@ +amends "elide:project.pkl" +import "elide:JavaScript.pkl" as js + +name = "db-studio-api" +version = "1.0.0" +description = "Database Studio API Server" + +entrypoint { + "index.ts" +} + +dependencies { + npm { + packages { + "zod@4" + } + + devPackages { + "@elide-dev/types@1.0.0-beta10" + } + } +} \ No newline at end of file diff --git a/packages/cli/src/db-studio/api/http/middleware.ts b/packages/cli/src/db-studio/api/http/middleware.ts new file mode 100644 index 0000000000..5bbf7560a8 --- /dev/null +++ b/packages/cli/src/db-studio/api/http/middleware.ts @@ -0,0 +1,20 @@ +import { Database } from "elide:sqlite"; +import type { ApiResponse, RouteContext, RouteHandler, DatabaseHandler } from "./types.ts"; +import { validateDatabaseId } from "../utils/validation.ts"; + +/** + * Middleware that validates database ID and provides database instance to handler + */ +export function withDatabase(handler: DatabaseHandler): RouteHandler { + return async (context: RouteContext): Promise => { + const { params, databases } = context; + const result = validateDatabaseId(params.dbId, databases); + if ("error" in result) return result.error; + + const { database } = result; + const db = new Database(database.path); + + return handler({ ...context, database, db }); + }; +} + diff --git a/packages/cli/src/db-studio/api/http/responses.ts b/packages/cli/src/db-studio/api/http/responses.ts new file mode 100644 index 0000000000..1e72ce3d91 --- /dev/null +++ b/packages/cli/src/db-studio/api/http/responses.ts @@ -0,0 +1,128 @@ +import type { ApiResponse } from "./types.ts"; + +const SUCCESS_STATUS = 200; +const ERROR_STATUS = 500; +const NOT_FOUND_STATUS = 404; + +/** + * CORS headers to allow cross-origin requests + */ +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}; + +/** + * Extract error message from various error types + * Handles standard Error, SQLite errors, and unknown error shapes + */ +export function extractErrorMessage(err: unknown): string { + if (err instanceof Error) { + return err.message; + } + + // Handle objects with message property (common in SQLite bindings) + if (typeof err === 'object' && err !== null) { + const errObj = err as Record; + + // Try common error message properties + if (typeof errObj.message === 'string') { + return errObj.message; + } + if (typeof errObj.error === 'string') { + return errObj.error; + } + if (typeof errObj.msg === 'string') { + return errObj.msg; + } + + // Try toString if available + if (typeof errObj.toString === 'function') { + const str = errObj.toString(); + if (str !== '[object Object]') { + return str; + } + } + + // Last resort: stringify the object + try { + return JSON.stringify(err); + } catch { + return 'Unknown error (could not serialize)'; + } + } + + // Handle primitive errors (strings, numbers) + if (typeof err === 'string') { + return err; + } + + return `Unknown error: ${String(err)}`; +} + +/** + * Create a JSON response + */ +export function jsonResponse(data: unknown, status: number = SUCCESS_STATUS): ApiResponse { + // console.log("returning json response", data); + return { + status, + headers: { "Content-Type": "application/json", ...corsHeaders }, + body: JSON.stringify(data), + }; +} + +/** + * Create a CORS preflight response for OPTIONS requests + */ +export function corsPreflightResponse(): ApiResponse { + return { + status: 204, + headers: corsHeaders, + body: "", + }; +} + +/** + * Create an error response + */ +export function errorResponse(message: string, status: number = ERROR_STATUS): ApiResponse { + return jsonResponse({ success: false, error: message }, status); +} + +/** + * Handle database operation errors + */ +export function handleDatabaseError(err: unknown, operation: string): ApiResponse { + console.error(`Database error during ${operation}:`, err); + const errorMessage = extractErrorMessage(err); + return errorResponse(`Failed to ${operation}: ${errorMessage}`, ERROR_STATUS); +} + +/** + * Handle SQL query errors with context + */ +export function handleSQLError( + err: unknown, + sql: string, + startTime?: number +): ApiResponse { + console.error("SQL error:", err); + const errorMessage = extractErrorMessage(err); + const endTime = startTime ? performance.now() : undefined; + + return jsonResponse({ + success: false, + error: errorMessage, + sql: sql.trim(), + executionTimeMs: endTime && startTime ? Number((endTime - startTime).toFixed(2)) : undefined, + }, ERROR_STATUS); +} + +/** + * Creates a 404 Not Found response + */ +export function notFoundResponse(): ApiResponse { + return jsonResponse({ error: "Not Found" }, NOT_FOUND_STATUS); +} diff --git a/packages/cli/src/db-studio/api/http/router.ts b/packages/cli/src/db-studio/api/http/router.ts new file mode 100644 index 0000000000..a74e436c95 --- /dev/null +++ b/packages/cli/src/db-studio/api/http/router.ts @@ -0,0 +1,97 @@ +/** + * Represents a captured route parameter + */ +type RouteParameter = { + name: string; + value: string; +}; + +/** + * Result of matching a single path segment + */ +type SegmentMatchResult = + | { matched: true; parameter: RouteParameter | null } + | { matched: false }; + +/** + * Splits a URL path into segments, filtering out empty strings + */ +function splitPathIntoSegments(path: string): string[] { + return path.split("/").filter(segment => segment.length > 0); +} + +/** + * Checks if a pattern segment is a parameter (starts with ':') + */ +function isParameterSegment(segment: string): boolean { + return segment.startsWith(":"); +} + +/** + * Extracts the parameter name from a pattern segment (removes the ':' prefix) + */ +function extractParameterName(segment: string): string { + return segment.slice(1); +} + +/** + * Matches a single pattern segment against a path segment + * Returns match result indicating success and any captured parameter + */ +function matchPathSegment( + patternSegment: string, + pathSegment: string +): SegmentMatchResult { + if (isParameterSegment(patternSegment)) { + return { + matched: true, + parameter: { + name: extractParameterName(patternSegment), + value: decodeURIComponent(pathSegment) + } + }; + } + + // Literal segment - must match exactly + if (patternSegment === pathSegment) { + return { matched: true, parameter: null }; + } + + return { matched: false }; +} + +/** + * Matches a route pattern against a path and extracts parameters + * + * Pattern segments starting with ':' are treated as parameters. + * + * @example + * matchRoute("/api/:dbId/query", "/api/0/query") + * // Returns: { dbId: "0" } + */ +export function matchRoute(pattern: string, path: string): Record | null { + const patternSegments = splitPathIntoSegments(pattern); + const pathSegments = splitPathIntoSegments(path); + + // Paths must have the same number of segments to match + if (patternSegments.length !== pathSegments.length) { + return null; + } + + const params: Record = {}; + + for (let i = 0; i < patternSegments.length; i++) { + const result = matchPathSegment(patternSegments[i], pathSegments[i]); + + if (!result.matched) { + return null; + } + + if (result.parameter) { + params[result.parameter.name] = result.parameter.value; + } + } + + return params; +} + diff --git a/packages/cli/src/db-studio/api/http/schemas.ts b/packages/cli/src/db-studio/api/http/schemas.ts new file mode 100644 index 0000000000..56a1f58515 --- /dev/null +++ b/packages/cli/src/db-studio/api/http/schemas.ts @@ -0,0 +1,319 @@ +import { z } from "zod/v3"; + +/** + * Foreign key reference information + */ +export const ForeignKeyReferenceSchema = z.object({ + table: z.string(), + column: z.string(), + onUpdate: z.string().optional(), + onDelete: z.string().optional(), +}); + +export type ForeignKeyReference = z.infer; + +/** + * Column metadata schema + * Includes type information, constraints, and relationships + */ +export const ColumnMetadataSchema = z.object({ + name: z.string(), + type: z.string(), + nullable: z.boolean(), + primaryKey: z.boolean(), + defaultValue: z.union([z.string(), z.number(), z.null()]).optional(), + foreignKey: ForeignKeyReferenceSchema.optional(), + unique: z.boolean().optional(), + autoIncrement: z.boolean().optional(), +}); + +export type ColumnMetadata = z.infer; + +/** + * Query execution metadata + */ +export const QueryMetadataSchema = z.object({ + executionTimeMs: z.number(), + sql: z.string(), + rowCount: z.number().optional(), // Not available for write queries +}); + +export type QueryMetadata = z.infer; + +/** + * SELECT query result schema + */ +export const SelectQueryResultSchema = z.object({ + success: z.literal(true), + data: z.array(z.record(z.string(), z.unknown())), + columns: z.array(ColumnMetadataSchema), + metadata: QueryMetadataSchema, +}); + +export type SelectQueryResult = z.infer; + +/** + * Write query (INSERT/UPDATE/DELETE) result schema + * Note: rowsAffected and lastInsertRowid are not currently available from elide sqlite + */ +export const WriteQueryResultSchema = z.object({ + success: z.literal(true), + metadata: QueryMetadataSchema, +}); + +export type WriteQueryResult = z.infer; + +/** + * Union of all successful query results + */ +export const QueryResultSchema = z.union([ + SelectQueryResultSchema, + WriteQueryResultSchema, +]); + +export type QueryResult = z.infer; + +/** + * Table data schema with enhanced column metadata + */ +export const TableDataSchema = z.object({ + name: z.string(), + columns: z.array(ColumnMetadataSchema), + rows: z.array(z.array(z.unknown())), + totalRows: z.number(), + metadata: QueryMetadataSchema, +}); + +export type TableDataResponse = z.infer; + +/** + * Error response schema + */ +export const ErrorResponseSchema = z.object({ + success: z.literal(false), + error: z.string(), + details: z.string().optional(), + sql: z.string().optional(), // The SQL query that caused the error + executionTimeMs: z.number().optional(), +}); + +export type ErrorResponse = z.infer; + +/** + * Filter operator types for WHERE clause filtering + */ +export type FilterOperator = + | 'eq' // equals (=) + | 'neq' // not equals (<>) + | 'gt' // greater than (>) + | 'gte' // greater or equal (>=) + | 'lt' // less than (<) + | 'lte' // less or equal (<=) + | 'like' // LIKE + | 'not_like' // NOT LIKE + | 'in' // IN (value is array) + | 'is_null' // IS NULL (no value) + | 'is_not_null' // IS NOT NULL (no value) + +/** + * Filter for WHERE clause conditions + */ +export type Filter = { + column: string + operator: FilterOperator + value?: string | number | null | string[] // undefined for is_null/is_not_null, array for 'in' +} + +/** + * Filter schema for runtime validation + */ +export const FilterSchema = z.object({ + column: z.string().min(1, "Column name cannot be empty"), + operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'like', 'not_like', 'in', 'is_null', 'is_not_null']), + value: z.union([z.string(), z.number(), z.null(), z.array(z.string())]).optional(), +}); + +export const FiltersArraySchema = z.array(FilterSchema); + +/** + * Delete rows request schema + * Expects an array of primary key objects + */ +export const DeleteRowsRequestSchema = z.object({ + primaryKeys: z.array(z.record(z.string(), z.unknown())).min(1, "At least one primary key required"), +}); + +export type DeleteRowsRequest = z.infer; + +/** + * Delete rows response schema + * Note: rowsAffected is not currently available from elide sqlite + */ +export const DeleteRowsResponseSchema = z.object({ + success: z.literal(true), +}); + +export type DeleteRowsResponse = z.infer; + +/** + * Execute query request schema + */ +export const ExecuteQueryRequestSchema = z.object({ + sql: z.string().min(1, "SQL query cannot be empty"), + params: z.array(z.unknown()).optional(), +}); + +export type ExecuteQueryRequest = z.infer; + +/** + * Column definition for table creation/editing + */ +export const ColumnDefinitionSchema = z.object({ + name: z.string().min(1, "Column name cannot be empty"), + type: z.enum(['INTEGER', 'TEXT', 'REAL', 'BLOB', 'NUMERIC']), + nullable: z.boolean(), + primaryKey: z.boolean(), + autoIncrement: z.boolean().optional(), + unique: z.boolean(), + defaultValue: z.union([z.string(), z.number(), z.null()]), +}); + +export type ColumnDefinition = z.infer; + +/** + * Create table column schema (legacy) + */ +export const CreateTableColumnSchema = z.object({ + name: z.string().min(1, "Column name cannot be empty"), + type: z.string().min(1, "Column type cannot be empty"), + constraints: z.string().optional(), +}); + +/** + * Create table request schema (updated to support new editor) + */ +export const CreateTableRequestSchema = z.union([ + // New format from table editor + z.object({ + name: z.string().min(1, "Table name cannot be empty"), + columns: z.array(ColumnDefinitionSchema).min(1, "At least one column required"), + }).refine( + (data) => { + const pkCount = data.columns.filter(c => c.primaryKey).length; + return pkCount <= 1; + }, + { message: "Maximum one primary key column allowed" } + ).refine( + (data) => { + const names = data.columns.map(c => c.name.toLowerCase()); + return names.length === new Set(names).size; + }, + { message: "Column names must be unique" } + ), + // Legacy format (for backwards compatibility) + z.object({ + name: z.string().min(1, "Table name cannot be empty"), + schema: z.array(CreateTableColumnSchema).min(1, "Schema must contain at least one column"), + }), +]); + +export type CreateTableRequest = z.infer; + +/** + * ALTER TABLE operations + */ +export const AddColumnOperationSchema = z.object({ + type: z.literal('add_column'), + column: z.object({ + name: z.string().min(1, "Column name cannot be empty"), + type: z.enum(['INTEGER', 'TEXT', 'REAL', 'BLOB', 'NUMERIC']), + nullable: z.boolean(), + defaultValue: z.union([z.string(), z.number(), z.null()]), + }), +}); + +export const DropColumnOperationSchema = z.object({ + type: z.literal('drop_column'), + columnName: z.string().min(1), +}); + +export const RenameColumnOperationSchema = z.object({ + type: z.literal('rename_column'), + oldName: z.string().min(1), + newName: z.string().min(1), +}); + +export const AlterTableOperationSchema = z.discriminatedUnion('type', [ + AddColumnOperationSchema, + DropColumnOperationSchema, + RenameColumnOperationSchema, +]); + +export type AlterTableOperation = z.infer; + +export const AlterTableRequestSchema = z.object({ + operations: z.array(AlterTableOperationSchema).min(1, "At least one operation required"), +}); + +export type AlterTableRequest = z.infer; + +/** + * Table schema response + */ +export const TableSchemaResponseSchema = z.object({ + tableName: z.string(), + columns: z.array(ColumnDefinitionSchema), +}); + +export type TableSchemaResponse = z.infer; + +/** + * Insert row request schema + * Expects an object mapping column names to values + */ +export const InsertRowRequestSchema = z.object({ + row: z.record(z.string(), z.unknown()).refine( + (data) => Object.keys(data).length > 0, + { message: "Row must contain at least one column" } + ), +}); + +export type InsertRowRequest = z.infer; + +/** + * Insert row response schema + * Note: rowsAffected and lastInsertRowid are not currently available from elide sqlite + */ +export const InsertRowResponseSchema = z.object({ + success: z.literal(true), +}); + +export type InsertRowResponse = z.infer; + +/** + * Update row request schema + * Expects a primary key object and an updates object + */ +export const UpdateRowRequestSchema = z.object({ + primaryKey: z.record(z.string(), z.unknown()).refine( + (data) => Object.keys(data).length > 0, + { message: "Primary key must contain at least one column" } + ), + updates: z.record(z.string(), z.unknown()).refine( + (data) => Object.keys(data).length > 0, + { message: "Updates must contain at least one column" } + ), +}); + +export type UpdateRowRequest = z.infer; + +/** + * Update row response schema + */ +export const UpdateRowResponseSchema = z.object({ + success: z.literal(true), + sql: z.string(), +}); + +export type UpdateRowResponse = z.infer; + diff --git a/packages/cli/src/db-studio/api/http/server.ts b/packages/cli/src/db-studio/api/http/server.ts new file mode 100644 index 0000000000..3f053c9285 --- /dev/null +++ b/packages/cli/src/db-studio/api/http/server.ts @@ -0,0 +1,60 @@ +import type { DiscoveredDatabase } from "../database.ts"; +import type { ApiResponse, RouteContext } from "./types.ts"; +import { matchRoute } from "./router.ts"; +import { routes } from "../routes/index.ts"; +import { errorResponse, notFoundResponse, extractErrorMessage } from "./responses.ts"; + +/** + * Extracts the path from a URL, removing query strings + */ +function parseUrlPath(url: string): string { + return url.split('?')[0]; +} + +/** + * Attempts to find and execute a matching route handler + */ +async function executeMatchingRoute( + path: string, + method: string, + context: RouteContext +): Promise { + for (const route of routes) { + if (route.method !== method) continue; + + const params = matchRoute(route.pattern, path); + if (params) { + return await route.handler({ ...context, params }); + } + } + + return null; +} + +/** + * Main API request handler - routes requests to appropriate handlers + */ +export async function handleApiRequest( + url: string, + method: string, + body: string, + databases: DiscoveredDatabase[] +): Promise { + try { + const path = parseUrlPath(url); + const context: RouteContext = { + databases, + params: {}, + body, + url + }; + + const response = await executeMatchingRoute(path, method, context); + return response ?? notFoundResponse(); + + } catch (err) { + console.error("Error handling request:", err); + return errorResponse(extractErrorMessage(err)); + } +} + diff --git a/packages/cli/src/db-studio/api/http/types.ts b/packages/cli/src/db-studio/api/http/types.ts new file mode 100644 index 0000000000..7020d773d8 --- /dev/null +++ b/packages/cli/src/db-studio/api/http/types.ts @@ -0,0 +1,57 @@ +import type { Database } from "elide:sqlite"; +import type { DiscoveredDatabase } from "../database.ts"; + +/** + * API response structure + */ +export type ApiResponse = { + status: number; + headers: Record; + body: string; +}; + +/** + * Context passed to all route handlers + */ +export type RouteContext = { + databases: DiscoveredDatabase[]; + params: Record; + body: string; + url: string; +}; + +/** + * Route handler function signature + */ +export type RouteHandler = ( + context: RouteContext +) => Promise; + +/** + * Route definition + */ +export type Route = { + method: string; + pattern: string; + handler: RouteHandler; +}; + +/** + * Extended context for database-specific handlers + */ +export type DatabaseHandlerContext = { + database: DiscoveredDatabase; + db: Database; + databases: DiscoveredDatabase[]; + params: Record; + body: string; + url: string; +}; + +/** + * Database-specific handler function signature + */ +export type DatabaseHandler = ( + context: DatabaseHandlerContext +) => Promise; + diff --git a/packages/cli/src/db-studio/api/index.ts b/packages/cli/src/db-studio/api/index.ts new file mode 100644 index 0000000000..4c4998d3ad --- /dev/null +++ b/packages/cli/src/db-studio/api/index.ts @@ -0,0 +1,73 @@ +import { createServer, IncomingMessage, ServerResponse } from "http"; +import { handleApiRequest } from "./http/server.ts"; +import { errorResponse, corsPreflightResponse } from "./http/responses.ts"; +import type { ApiResponse } from "./http/types.ts"; +import config from "./config.ts"; + +/** + * Database Studio - Entry Point + * + * Bootstraps the HTTP server for the Database Studio API. + */ + +const { port, databases } = config; + +/** + * Parses the request body from an incoming HTTP request + */ +function parseRequestBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + + req.on('data', (chunk) => { + body += chunk.toString('utf8'); + }); + + req.on('end', () => resolve(body)); + req.on('error', (err) => reject(err)); + }); +} + +/** + * Writes an ApiResponse to the HTTP ServerResponse + */ +function writeResponse(res: ServerResponse, response: ApiResponse): void { + res.writeHead(response.status, { + ...response.headers, + 'Content-Length': Buffer.byteLength(response.body, 'utf8') + }); + res.end(response.body); +} + +/** + * Main request handler - processes incoming HTTP requests + */ +async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise { + try { + const method = req.method || 'GET'; + + // Handle CORS preflight requests + if (method === 'OPTIONS') { + writeResponse(res, corsPreflightResponse()); + return; + } + + const body = await parseRequestBody(req); + const url = req.url || '/'; + + const response = await handleApiRequest(url, method, body, databases); + writeResponse(res, response); + } catch (err) { + console.error("Error handling request:", err); + const response = errorResponse('Internal server error', 500); + writeResponse(res, response); + } +} + +// Create and configure HTTP server +const server = createServer(handleRequest); + +// Start listening on configured port +server.listen(port, () => { + // console.log(`Database Studio API started on http://localhost:${port} 🚀`); +}); \ No newline at end of file diff --git a/packages/cli/src/db-studio/api/package-lock.kdl b/packages/cli/src/db-studio/api/package-lock.kdl new file mode 100644 index 0000000000..83c8462fcf --- /dev/null +++ b/packages/cli/src/db-studio/api/package-lock.kdl @@ -0,0 +1,35 @@ +lockfile-version 1 +root{ +dependencies{ +zod ">=4.0.0 <5.0.0-0" + } +dev-dependencies{ +"@elide-dev/types" "1.0.0-beta10" + } +} +pkg "@elide-dev/types"{ +version "1.0.0-beta10" +resolved "https://registry.npmjs.org/@elide-dev/types/-/types-1.0.0-beta10.tgz" +integrity "sha512-VAaiwprV0ekDHn0cXxn/GnvNohm2gLk0SOqymMCyWK6Tk0Q9rf0O4PCR4NGOFZf51zfghB1Pd204w72lhQbPhg==" +dependencies{ +"@types/node" ">=0.0.0" + } +} +pkg "@types/node"{ +version "24.10.1" +resolved "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz" +integrity "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==" +dependencies{ +undici-types ">=7.16.0 <7.17.0-0" + } +} +pkg undici-types{ +version "7.16.0" +resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz" +integrity "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" +} +pkg zod{ +version "4.1.12" +resolved "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz" +integrity "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==" +} diff --git a/packages/cli/src/db-studio/api/routes/databases.ts b/packages/cli/src/db-studio/api/routes/databases.ts new file mode 100644 index 0000000000..c57ab6e344 --- /dev/null +++ b/packages/cli/src/db-studio/api/routes/databases.ts @@ -0,0 +1,30 @@ +import type { RouteContext, ApiResponse } from "../http/types.ts"; +import { jsonResponse } from "../http/responses.ts"; +import { withDatabase } from "../http/middleware.ts"; +import { getDatabaseInfo } from "../database.ts"; + +/** + * List all databases + */ +export async function listDatabases(context: RouteContext): Promise { + const { databases } = context; + return jsonResponse({ databases }); +} + +/** + * Get database info + */ +export const getDatabaseInfoRoute = withDatabase(async (context) => { + const { db, database } = context; + const info = getDatabaseInfo(db, database.path); + + const fullInfo = { + ...info, + size: database.size, + lastModified: database.lastModified, + tableCount: info.tableCount, + }; + + return jsonResponse(fullInfo); +}); + diff --git a/packages/cli/src/db-studio/api/routes/health.ts b/packages/cli/src/db-studio/api/routes/health.ts new file mode 100644 index 0000000000..9abbf41971 --- /dev/null +++ b/packages/cli/src/db-studio/api/routes/health.ts @@ -0,0 +1,10 @@ +import type { ApiResponse, RouteContext } from "../http/types.ts"; +import { jsonResponse } from "../http/responses.ts"; + +/** + * Health check endpoint + */ +export async function healthCheck(_context: RouteContext): Promise { + return jsonResponse({ status: "ok" }); +} + diff --git a/packages/cli/src/db-studio/api/routes/index.ts b/packages/cli/src/db-studio/api/routes/index.ts new file mode 100644 index 0000000000..3def9b4ec6 --- /dev/null +++ b/packages/cli/src/db-studio/api/routes/index.ts @@ -0,0 +1,41 @@ +import type { Route } from "../http/types.ts"; +import { healthCheck } from "./health.ts"; +import { listDatabases, getDatabaseInfoRoute } from "./databases.ts"; +import { getTablesRoute, getTableDataRoute, createTableRoute, dropTableRoute, truncateTableRoute, getTableSchemaRoute, alterTableRoute } from "./tables.ts"; +import { executeQueryRoute } from "./query.ts"; +import { deleteRowsRoute, insertRowRoute, updateRowRoute } from "./rows.ts"; + +/** + * Route Registry + * + * All application routes are defined here, mapping HTTP methods and URL patterns + * to their corresponding handler functions. + */ +export const routes: Route[] = [ + // Health check + { method: "GET", pattern: "/health", handler: healthCheck }, + + // Database operations + { method: "GET", pattern: "/api/databases", handler: listDatabases }, + { method: "GET", pattern: "/api/databases/:dbId", handler: getDatabaseInfoRoute }, + + // Table operations - read + { method: "GET", pattern: "/api/databases/:dbId/tables", handler: getTablesRoute }, + { method: "GET", pattern: "/api/databases/:dbId/tables/:tableName", handler: getTableDataRoute }, + { method: "GET", pattern: "/api/databases/:dbId/tables/:tableName/schema", handler: getTableSchemaRoute }, + + // Table operations - write (structure-level operations) + { method: "POST", pattern: "/api/databases/:dbId/tables", handler: createTableRoute }, + { method: "PUT", pattern: "/api/databases/:dbId/tables/:tableName", handler: alterTableRoute }, + { method: "DELETE", pattern: "/api/databases/:dbId/tables/:tableName", handler: dropTableRoute }, + { method: "POST", pattern: "/api/databases/:dbId/tables/:tableName/truncate", handler: truncateTableRoute }, + + // Row operations - write (data-level operations) + { method: "POST", pattern: "/api/databases/:dbId/tables/:tableName/rows", handler: insertRowRoute }, + { method: "PUT", pattern: "/api/databases/:dbId/tables/:tableName/rows", handler: updateRowRoute }, + { method: "DELETE", pattern: "/api/databases/:dbId/tables/:tableName/rows", handler: deleteRowsRoute }, + + // Raw SQL query endpoint (for custom queries) + { method: "POST", pattern: "/api/databases/:dbId/query", handler: executeQueryRoute }, +]; + diff --git a/packages/cli/src/db-studio/api/routes/query.ts b/packages/cli/src/db-studio/api/routes/query.ts new file mode 100644 index 0000000000..44c9549f3a --- /dev/null +++ b/packages/cli/src/db-studio/api/routes/query.ts @@ -0,0 +1,66 @@ +import { jsonResponse, handleSQLError, errorResponse } from "../http/responses.ts"; +import { withDatabase } from "../http/middleware.ts"; +import { parseRequestBody } from "../utils/request.ts"; +import { executeQuery, logQuery } from "../database.ts"; +import { ExecuteQueryRequestSchema } from "../http/schemas.ts"; + +/** + * Execute a raw SQL query with rich metadata + */ +export const executeQueryRoute = withDatabase(async (context) => { + const { db, body } = context; + const data = parseRequestBody(body); + const result = ExecuteQueryRequestSchema.safeParse(data); + + if (!result.success) { + return errorResponse( + `Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`, + 400 + ); + } + + const { sql, params: queryParams } = result.data; + const trimmedSql = sql.trim(); + const startTime = performance.now(); + + try { + const params = queryParams || []; + const sqlLower = trimmedSql.toLowerCase(); + + // Determine if this is a SELECT query + if (sqlLower.startsWith("select")) { + // For SELECT queries, use our enhanced executeQuery function + const { columns, data: rows } = executeQuery(db, trimmedSql); + const endTime = performance.now(); + + return jsonResponse({ + success: true, + data: rows, + columns, + metadata: { + executionTimeMs: Number((endTime - startTime).toFixed(2)), + sql: trimmedSql, + rowCount: rows.length, + }, + }); + } else { + // For INSERT/UPDATE/DELETE/CREATE/DROP etc. + // Note: rowsAffected and lastInsertRowid are not currently available from elide sqlite + logQuery(trimmedSql, params); + const stmt = db.prepare(trimmedSql); + stmt.run(...(params as any)); + const endTime = performance.now(); + + return jsonResponse({ + success: true, + metadata: { + executionTimeMs: Number((endTime - startTime).toFixed(2)), + sql: trimmedSql, + }, + }); + } + } catch (err) { + return handleSQLError(err, trimmedSql, startTime); + } +}); + diff --git a/packages/cli/src/db-studio/api/routes/rows.ts b/packages/cli/src/db-studio/api/routes/rows.ts new file mode 100644 index 0000000000..f0daae0443 --- /dev/null +++ b/packages/cli/src/db-studio/api/routes/rows.ts @@ -0,0 +1,137 @@ +import { jsonResponse, extractErrorMessage } from "../http/responses.ts"; +import { withDatabase } from "../http/middleware.ts"; +import { requireTableName } from "../utils/validation.ts"; +import { parseRequestBody } from "../utils/request.ts"; +import { deleteRows, insertRow, updateRow, SQLError } from "../database.ts"; +import { DeleteRowsRequestSchema, InsertRowRequestSchema, UpdateRowRequestSchema } from "../http/schemas.ts"; + +/** + * Delete rows from a table based on primary key values + * DELETE /api/databases/:dbId/tables/:tableName/rows + * + * Request body: { primaryKeys: [{ id: 1 }, { id: 2 }] } + * Response: { success: true } + */ +export const deleteRowsRoute = withDatabase(async (context) => { + const { params, db, body } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + // Parse and validate request body + const data = parseRequestBody(body); + const result = DeleteRowsRequestSchema.safeParse(data); + + if (!result.success) { + return jsonResponse({ + success: false, + error: `Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`, + }, 400); + } + + const { primaryKeys } = result.data; + + try { + deleteRows(db, params.tableName, primaryKeys); + return jsonResponse({ + success: true, + }); + } catch (err) { + console.error("Delete rows error:", err); + + // SQLError includes the SQL that was executed + if (err instanceof SQLError) { + return jsonResponse({ success: false, error: err.message, sql: err.sql }, 400); + } + + const errorMessage = extractErrorMessage(err); + return jsonResponse({ success: false, error: errorMessage }, 400); + } +}); + +/** + * Insert a new row into a table + * POST /api/databases/:dbId/tables/:tableName/rows + * + * Request body: { row: { column1: value1, column2: value2 } } + * Response: { success: true } + */ +export const insertRowRoute = withDatabase(async (context) => { + const { params, db, body } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + // Parse and validate request body + const data = parseRequestBody(body); + const result = InsertRowRequestSchema.safeParse(data); + + if (!result.success) { + return jsonResponse({ + success: false, + error: `Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`, + }, 400); + } + + const { row } = result.data; + + try { + const result = insertRow(db, params.tableName, row); + return jsonResponse({ + success: true, + sql: result.sql, + }); + } catch (err) { + console.error("Insert row error:", err); + + // SQLError includes the SQL that was executed + if (err instanceof SQLError) { + return jsonResponse({ success: false, error: err.message, sql: err.sql }, 400); + } + + const errorMessage = extractErrorMessage(err); + return jsonResponse({ success: false, error: errorMessage }, 400); + } +}); + +/** + * Update a row in a table based on primary key + * PUT /api/databases/:dbId/tables/:tableName/rows + * + * Request body: { primaryKey: { id: 1 }, updates: { column1: newValue } } + * Response: { success: true, sql: "UPDATE..." } + */ +export const updateRowRoute = withDatabase(async (context) => { + const { params, db, body } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + // Parse and validate request body + const data = parseRequestBody(body); + const result = UpdateRowRequestSchema.safeParse(data); + + if (!result.success) { + return jsonResponse({ + success: false, + error: `Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`, + }, 400); + } + + const { primaryKey, updates } = result.data; + + try { + const result = updateRow(db, params.tableName, primaryKey, updates); + return jsonResponse({ + success: true, + sql: result.sql, + }); + } catch (err) { + console.error("Update row error:", err); + + // SQLError includes the SQL that was executed + if (err instanceof SQLError) { + return jsonResponse({ success: false, error: err.message, sql: err.sql }, 400); + } + + const errorMessage = extractErrorMessage(err); + return jsonResponse({ success: false, error: errorMessage }, 400); + } +}); diff --git a/packages/cli/src/db-studio/api/routes/tables.ts b/packages/cli/src/db-studio/api/routes/tables.ts new file mode 100644 index 0000000000..78820bacea --- /dev/null +++ b/packages/cli/src/db-studio/api/routes/tables.ts @@ -0,0 +1,320 @@ +import { jsonResponse, handleSQLError, errorResponse, extractErrorMessage } from "../http/responses.ts"; +import { withDatabase } from "../http/middleware.ts"; +import { requireTableName } from "../utils/validation.ts"; +import { parseRequestBody, parseQueryParams } from "../utils/request.ts"; +import { getTables, getTableData, getColumnMetadata, logQuery } from "../database.ts"; +import type { Filter } from "../http/schemas.ts"; +import { CreateTableRequestSchema, FiltersArraySchema, AlterTableRequestSchema } from "../http/schemas.ts"; + +/** + * Get list of tables in a database + */ +export const getTablesRoute = withDatabase(async (context) => { + const { db } = context; + const tables = getTables(db); + return jsonResponse({ tables }); +}); + +/** + * Get table data with enhanced column metadata + * Supports query parameters: limit (default: 100), offset (default: 0), sort (column name), order (asc/desc), where (JSON-encoded filters) + */ +export const getTableDataRoute = withDatabase(async (context) => { + const { params, db, url } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + // Parse query parameters for pagination and sorting + const queryParams = parseQueryParams(url); + const limitParam = queryParams.get('limit'); + const offsetParam = queryParams.get('offset'); + const sortParam = queryParams.get('sort'); + const orderParam = queryParams.get('order'); + const whereParam = queryParams.get('where'); + + const limit = limitParam ? parseInt(limitParam, 10) : 100; + const offset = offsetParam ? parseInt(offsetParam, 10) : 0; + + // Validate pagination parameters + if (isNaN(limit) || limit < 1 || limit > 1000) { + return errorResponse("Invalid limit parameter (must be between 1 and 1000)", 400); + } + if (isNaN(offset) || offset < 0) { + return errorResponse("Invalid offset parameter (must be >= 0)", 400); + } + + // Validate sorting parameters + let sortColumn: string | null = null; + let sortDirection: 'asc' | 'desc' | null = null; + + if (sortParam) { + // Validate order parameter if sort is provided + if (!orderParam || (orderParam !== 'asc' && orderParam !== 'desc')) { + return errorResponse("Invalid order parameter (must be 'asc' or 'desc' when sort is provided)", 400); + } + sortColumn = sortParam; + sortDirection = orderParam as 'asc' | 'desc'; + } + + // Parse and validate filters + let filters: Filter[] | null = null; + if (whereParam) { + try { + const decodedWhere = decodeURIComponent(whereParam); + const parsedWhere = JSON.parse(decodedWhere); + + // Validate using zod schema + const result = FiltersArraySchema.safeParse(parsedWhere); + if (!result.success) { + return errorResponse( + `Invalid where parameter: ${result.error.errors.map(e => e.message).join(", ")}`, + 400 + ); + } + + filters = result.data; + } catch (err) { + console.error("Error parsing where parameter:", err); + return errorResponse( + `Failed to parse where parameter: ${extractErrorMessage(err)}`, + 400 + ); + } + } + + try { + const tableData = getTableData(db, params.tableName, limit, offset, sortColumn, sortDirection, filters); + return jsonResponse(tableData); + } catch (err) { + console.error("Error getting table data:", err); + return errorResponse(extractErrorMessage(err), 400); + } +}); + +/** + * Create a new table + */ +export const createTableRoute = withDatabase(async (context) => { + const { db, body } = context; + const data = parseRequestBody(body); + const result = CreateTableRequestSchema.safeParse(data); + + if (!result.success) { + return errorResponse( + `Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`, + 400 + ); + } + + const tableName = result.data.name; + let sql: string; + + // Check if using new format (with 'columns') or legacy format (with 'schema') + if ('columns' in result.data) { + // New format from table editor + const { columns } = result.data; + + const columnDefs = columns.map(col => { + const parts = [`"${col.name}" ${col.type}`]; + + if (col.primaryKey) { + parts.push('PRIMARY KEY'); + // AUTOINCREMENT only valid for INTEGER PRIMARY KEY + if (col.autoIncrement && col.type === 'INTEGER') { + parts.push('AUTOINCREMENT'); + } + } + + if (!col.nullable && !col.primaryKey) parts.push('NOT NULL'); + if (col.unique && !col.primaryKey) parts.push('UNIQUE'); + + if (col.defaultValue !== null) { + const val = typeof col.defaultValue === 'string' + ? `'${col.defaultValue.replace(/'/g, "''")}'` + : col.defaultValue; + parts.push(`DEFAULT ${val}`); + } + + return parts.join(' '); + }); + + sql = `CREATE TABLE "${tableName}" (${columnDefs.join(', ')})`; + } else { + // Legacy format + const { schema } = result.data; + const columns = schema.map(col => { + const constraints = col.constraints ? ` ${col.constraints}` : ""; + return `"${col.name}" ${col.type}${constraints}`; + }).join(", "); + sql = `CREATE TABLE "${tableName}" (${columns})`; + } + + const startTime = performance.now(); + + try { + logQuery(sql); + db.exec(sql); + return jsonResponse({ success: true, message: `Table '${tableName}' created successfully` }); + } catch (err) { + return handleSQLError(err, sql, startTime); + } +}); + +/** + * Drop a table + */ +export const dropTableRoute = withDatabase(async (context) => { + const { params, db } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + const sql = `DROP TABLE "${params.tableName}"`; + const startTime = performance.now(); + + try { + logQuery(sql); + db.exec(sql); + return jsonResponse({ success: true, message: `Table '${params.tableName}' dropped successfully` }); + } catch (err) { + return handleSQLError(err, sql, startTime); + } +}); + +/** + * Truncate a table (delete all rows) + */ +export const truncateTableRoute = withDatabase(async (context) => { + const { params, db } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + const sql = `DELETE FROM "${params.tableName}"`; + const startTime = performance.now(); + + try { + logQuery(sql); + db.exec(sql); + return jsonResponse({ success: true, message: `Table '${params.tableName}' truncated successfully` }); + } catch (err) { + return handleSQLError(err, sql, startTime); + } +}); + +/** + * Get table schema (for editing) + */ +export const getTableSchemaRoute = withDatabase(async (context) => { + const { params, db } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + try { + const columns = getColumnMetadata(db, params.tableName); + + // Map to our schema format (simplified from full metadata) + const schema = columns.map(col => ({ + name: col.name, + type: col.type, + nullable: col.nullable, + primaryKey: col.primaryKey, + autoIncrement: col.autoIncrement || false, + unique: col.unique || false, + defaultValue: col.defaultValue || null, + })); + + return jsonResponse({ + tableName: params.tableName, + columns: schema, + }); + } catch (err) { + console.error("Error getting table schema:", err); + return errorResponse(extractErrorMessage(err), 400); + } +}); + +/** + * Alter existing table structure + */ +export const alterTableRoute = withDatabase(async (context) => { + const { params, db, body } = context; + const tableNameError = requireTableName(params); + if (tableNameError) return tableNameError; + + const data = parseRequestBody(body); + const result = AlterTableRequestSchema.safeParse(data); + + if (!result.success) { + return errorResponse( + `Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`, + 400 + ); + } + + const { operations } = result.data; + const tableName = params.tableName; + const executedStatements: string[] = []; + + try { + // Execute operations in a transaction + db.transaction(() => { + for (const op of operations) { + let sql: string; + + switch (op.type) { + case 'add_column': { + const { column } = op; + const parts = [`"${column.name}" ${column.type}`]; + + if (!column.nullable) { + // NOT NULL requires DEFAULT for existing rows + if (column.defaultValue === null) { + throw new Error( + `Cannot add NOT NULL column '${column.name}' without a default value` + ); + } + parts.push('NOT NULL'); + } + + if (column.defaultValue !== null) { + const val = typeof column.defaultValue === 'string' + ? `'${column.defaultValue.replace(/'/g, "''")}'` + : column.defaultValue; + parts.push(`DEFAULT ${val}`); + } + + sql = `ALTER TABLE "${tableName}" ADD COLUMN ${parts.join(' ')}`; + break; + } + + case 'drop_column': { + sql = `ALTER TABLE "${tableName}" DROP COLUMN "${op.columnName}"`; + break; + } + + case 'rename_column': { + sql = `ALTER TABLE "${tableName}" RENAME COLUMN "${op.oldName}" TO "${op.newName}"`; + break; + } + + default: + throw new Error(`Unknown operation type: ${(op as any).type}`); + } + + logQuery(sql); + db.exec(sql); + executedStatements.push(sql); + } + })(); + + return jsonResponse({ + success: true, + message: `Table '${tableName}' altered successfully`, + executedStatements, + }); + } catch (err) { + console.error("Error altering table:", err); + const message = extractErrorMessage(err); + return errorResponse(message, 400); + } +}); + diff --git a/packages/cli/src/db-studio/api/tsconfig.json b/packages/cli/src/db-studio/api/tsconfig.json new file mode 100644 index 0000000000..979acea239 --- /dev/null +++ b/packages/cli/src/db-studio/api/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "skipLibCheck": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "types": ["@elide-dev/types"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/cli/src/db-studio/api/utils/request.ts b/packages/cli/src/db-studio/api/utils/request.ts new file mode 100644 index 0000000000..9244c4b312 --- /dev/null +++ b/packages/cli/src/db-studio/api/utils/request.ts @@ -0,0 +1,60 @@ +/** + * Parse query parameters from URL string + * Extracts and decodes query parameters without using URL API (not available in Elide runtime) + */ +export function parseQueryParams(url: string): Map { + const queryParams = new Map(); + const queryIndex = url.indexOf('?'); + + if (queryIndex < 0) { + return queryParams; + } + + const queryString = url.substring(queryIndex + 1); + if (!queryString) { + return queryParams; + } + + const pairs = queryString.split('&'); + for (const pair of pairs) { + const [key, value] = pair.split('='); + if (key && value !== undefined) { + queryParams.set(decodeURIComponent(key), decodeURIComponent(value)); + } + } + + return queryParams; +} + +/** + * Parse request body as JSON + */ +export function parseRequestBody(body: string): Record { + if (!body || body.trim() === "") { + return {}; + } + try { + return JSON.parse(body); + } catch { + return {}; + } +} + +/** + * Build SQL WHERE clause from object + */ +export function buildWhereClause(where: Record): { clause: string; values: unknown[] } { + const conditions: string[] = []; + const values: unknown[] = []; + + for (const [key, value] of Object.entries(where)) { + conditions.push(`"${key}" = ?`); + values.push(value); + } + + return { + clause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "", + values, + }; +} + diff --git a/packages/cli/src/db-studio/api/utils/validation.ts b/packages/cli/src/db-studio/api/utils/validation.ts new file mode 100644 index 0000000000..56e6a698b5 --- /dev/null +++ b/packages/cli/src/db-studio/api/utils/validation.ts @@ -0,0 +1,56 @@ +import type { ApiResponse } from "../http/types.ts"; +import type { DiscoveredDatabase } from "../database.ts"; +import { errorResponse } from "../http/responses.ts"; + +/** + * Validate database ID parameter + */ +export function validateDatabaseId( + dbId: string, + databases: DiscoveredDatabase[] +): { database: DiscoveredDatabase } | { error: ApiResponse } { + const database = databases.find(db => db.id === dbId); + if (!database) { + return { error: errorResponse(`Database not found with ID: ${dbId}`, 404) }; + } + return { database }; +} + +/** + * Validate database index parameter + * @deprecated Use validateDatabaseId instead + */ +export function validateDatabaseIndex( + dbId: string, + databases: DiscoveredDatabase[] +): { database: DiscoveredDatabase } | { error: ApiResponse } { + const database = databases.find(db => db.id === dbId); + if (!database) { + return { error: errorResponse(`Database not found with ID: ${dbId}`, 404) }; + } + return { database }; +} + +/** + * Require table name parameter + */ +export function requireTableName(params: Record): ApiResponse | null { + const tableName = params.tableName; + if (!tableName) { + return errorResponse("Table name is required", 400); + } + return null; +} + +/** + * Check if a SQL query is potentially destructive + */ +export function isDestructiveQuery(sql: string): boolean { + const normalized = sql.trim().toLowerCase(); + return ( + normalized.startsWith("drop") || + normalized.startsWith("truncate") || + (normalized.startsWith("delete") && !normalized.includes("where")) + ); +} + diff --git a/packages/cli/src/db-studio/ui/.gitignore b/packages/cli/src/db-studio/ui/.gitignore new file mode 100644 index 0000000000..335bd46df2 --- /dev/null +++ b/packages/cli/src/db-studio/ui/.gitignore @@ -0,0 +1,8 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/packages/cli/src/db-studio/ui/.prettierrc b/packages/cli/src/db-studio/ui/.prettierrc new file mode 100644 index 0000000000..21f638e762 --- /dev/null +++ b/packages/cli/src/db-studio/ui/.prettierrc @@ -0,0 +1,14 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "options": { + "parser": "typescript" + } + } + ] +} diff --git a/packages/cli/src/db-studio/ui/README.md b/packages/cli/src/db-studio/ui/README.md new file mode 100644 index 0000000000..4dcad1f915 --- /dev/null +++ b/packages/cli/src/db-studio/ui/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/packages/cli/src/db-studio/ui/components.json b/packages/cli/src/db-studio/ui/components.json new file mode 100644 index 0000000000..9b4169b45d --- /dev/null +++ b/packages/cli/src/db-studio/ui/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/packages/cli/src/db-studio/ui/dist/assets/index-BchqHb1q.css b/packages/cli/src/db-studio/ui/dist/assets/index-BchqHb1q.css new file mode 100644 index 0000000000..29d0a67524 --- /dev/null +++ b/packages/cli/src/db-studio/ui/dist/assets/index-BchqHb1q.css @@ -0,0 +1 @@ +/*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-500:oklch(63.7% .237 25.331);--color-red-900:oklch(39.6% .141 25.723);--color-amber-500:oklch(76.9% .188 70.08);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-emerald-500:oklch(69.6% .17 162.48);--color-blue-500:oklch(62.3% .214 259.815);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-xs:.125rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-background:var(--background);--color-foreground:var(--foreground);--color-card:var(--card);--color-card-foreground:var(--card-foreground);--color-popover:var(--popover);--color-popover-foreground:var(--popover-foreground);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground);--color-secondary:var(--secondary);--color-secondary-foreground:var(--secondary-foreground);--color-muted:var(--muted);--color-muted-foreground:var(--muted-foreground);--color-accent:var(--accent);--color-accent-foreground:var(--accent-foreground);--color-destructive:var(--destructive);--color-destructive-foreground:var(--destructive-foreground);--color-border:var(--border);--color-input:var(--input);--color-ring:var(--ring)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);color:var(--foreground)}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.not-sr-only{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.-top-1{top:calc(var(--spacing)*-1)}.top-0{top:calc(var(--spacing)*0)}.top-1\.5{top:calc(var(--spacing)*1.5)}.top-1\/2{top:50%}.top-3\.5{top:calc(var(--spacing)*3.5)}.top-4{top:calc(var(--spacing)*4)}.top-\[50\%\]{top:50%}.-right-1{right:calc(var(--spacing)*-1)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-1{left:calc(var(--spacing)*1)}.left-2{left:calc(var(--spacing)*2)}.left-4{left:calc(var(--spacing)*4)}.left-\[50\%\]{left:50%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[9999\]{z-index:9999}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.container\!{width:100%!important}@media(min-width:40rem){.container\!{max-width:40rem!important}}@media(min-width:48rem){.container\!{max-width:48rem!important}}@media(min-width:64rem){.container\!{max-width:64rem!important}}@media(min-width:80rem){.container\!{max-width:80rem!important}}@media(min-width:96rem){.container\!{max-width:96rem!important}}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-3\.5{margin-inline:calc(var(--spacing)*3.5)}.mx-auto{margin-inline:auto}.my-0{margin-block:calc(var(--spacing)*0)}.my-1{margin-block:calc(var(--spacing)*1)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-4{margin-right:calc(var(--spacing)*4)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-square{aspect-ratio:1}.size-2\.5{width:calc(var(--spacing)*2.5);height:calc(var(--spacing)*2.5)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-8{width:calc(var(--spacing)*8);height:calc(var(--spacing)*8)}.size-10{width:calc(var(--spacing)*10);height:calc(var(--spacing)*10)}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-16{height:calc(var(--spacing)*16)}.h-\[30px\]{height:30px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.h-svh{height:100svh}.max-h-32{max-height:calc(var(--spacing)*32)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[300px\]{max-height:300px}.max-h-\[var\(--radix-dropdown-menu-content-available-height\)\]{max-height:var(--radix-dropdown-menu-content-available-height)}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-\[60px\]{min-height:60px}.min-h-screen{min-height:100vh}.min-h-svh{min-height:100svh}.w-1{width:calc(var(--spacing)*1)}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-3\/4{width:75%}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-20{width:calc(var(--spacing)*20)}.w-64{width:calc(var(--spacing)*64)}.w-80{width:calc(var(--spacing)*80)}.w-\[--sidebar-width\]{width:--sidebar-width}.w-\[30px\]{width:30px}.w-\[160px\]{width:160px}.w-\[200px\]{width:200px}.w-\[240px\]{width:240px}.w-\[280px\]{width:280px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-px{width:1px}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[--skeleton-width\]{max-width:--skeleton-width}.max-w-\[280px\]{max-width:280px}.max-w-lg{max-width:var(--container-lg)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-5{min-width:calc(var(--spacing)*5)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[140px\]{min-width:140px}.min-w-\[200px\]{min-width:200px}.flex-1{flex:1}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.origin-\(--radix-tooltip-content-transform-origin\){transform-origin:var(--radix-tooltip-content-transform-origin)}.origin-\[--radix-dropdown-menu-content-transform-origin\]{transform-origin:--radix-dropdown-menu-content-transform-origin}.origin-\[--radix-hover-card-content-transform-origin\]{transform-origin:--radix-hover-card-content-transform-origin}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-px{--tw-translate-x:-1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-px{--tw-translate-x:1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[calc\(-50\%-2px\)\]{--tw-translate-y: calc(-50% - 2px) ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-none{translate:none}.scale-3d{scale:var(--tw-scale-x)var(--tw-scale-y)var(--tw-scale-z)}.-rotate-90{rotate:-90deg}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)!important}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-none{touch-action:none}.resize{resize:both}.resize\!{resize:both!important}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-reverse>:not(:last-child)){--tw-space-y-reverse:1}:where(.space-x-reverse>:not(:last-child)){--tw-space-x-reverse:1}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}.self-center{align-self:center}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[2px\]{border-radius:2px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-none{border-radius:0}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xs{border-radius:var(--radius-xs)}.rounded-s{border-start-start-radius:.25rem;border-end-start-radius:.25rem}.rounded-ss{border-start-start-radius:.25rem}.rounded-e{border-start-end-radius:.25rem;border-end-end-radius:.25rem}.rounded-se{border-start-end-radius:.25rem}.rounded-ee{border-end-end-radius:.25rem}.rounded-es{border-end-start-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-tl{border-top-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-tr{border-top-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-br{border-bottom-right-radius:.25rem}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-s{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.border-e{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--border)}.border-destructive,.border-destructive\/20{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/20{border-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/30{border-color:color-mix(in oklab,var(--destructive)30%,transparent)}}.border-destructive\/50{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/50{border-color:color-mix(in oklab,var(--destructive)50%,transparent)}}.border-input{border-color:var(--input)}.border-primary{border-color:var(--primary)}.border-transparent{border-color:#0000}.bg-accent,.bg-accent\/50{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/50{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.bg-background{background-color:var(--background)}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab,red,red)){.bg-black\/80{background-color:color-mix(in oklab,var(--color-black)80%,transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/20{background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-destructive,.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/5{background-color:color-mix(in oklab,var(--destructive)5%,transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.bg-foreground{background-color:var(--foreground)}.bg-green-600{background-color:var(--color-green-600)}.bg-muted,.bg-muted\/30{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/30{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.bg-popover{background-color:var(--popover)}.bg-primary,.bg-primary\/10{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-repeat{background-repeat:repeat}.mask-no-clip{-webkit-mask-clip:no-clip;mask-clip:no-clip}.mask-repeat{-webkit-mask-repeat:repeat;mask-repeat:repeat}.fill-current{fill:currentColor}.fill-foreground{fill:var(--foreground)}.p-0{padding:calc(var(--spacing)*0)}.p-0\.5{padding:calc(var(--spacing)*.5)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-0\.5{padding-top:calc(var(--spacing)*.5)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-6{padding-top:calc(var(--spacing)*6)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-8{padding-left:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.text-wrap{text-wrap:wrap}.break-all{word-break:break-all}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.text-accent-foreground{color:var(--accent-foreground)}.text-amber-500{color:var(--color-amber-500)}.text-background{color:var(--background)}.text-black{color:var(--color-black)}.text-blue-500{color:var(--color-blue-500)}.text-card-foreground{color:var(--card-foreground)}.text-current{color:currentColor}.text-destructive{color:var(--destructive)}.text-destructive-foreground{color:var(--destructive-foreground)}.text-destructive\/70{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/70{color:color-mix(in oklab,var(--destructive)70%,transparent)}}.text-destructive\/80{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/80{color:color-mix(in oklab,var(--destructive)80%,transparent)}}.text-destructive\/90{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/90{color:color-mix(in oklab,var(--destructive)90%,transparent)}}.text-emerald-500{color:var(--color-emerald-500)}.text-foreground{color:var(--foreground)}.text-green-500{color:var(--color-green-500)}.text-muted-foreground,.text-muted-foreground\/70{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/70{color:color-mix(in oklab,var(--muted-foreground)70%,transparent)}}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-red-500{color:var(--color-red-500)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.diagonal-fractions{--tw-numeric-fraction:diagonal-fractions;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.lining-nums{--tw-numeric-figure:lining-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.oldstyle-nums{--tw-numeric-figure:oldstyle-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.proportional-nums{--tw-numeric-spacing:proportional-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.slashed-zero{--tw-slashed-zero:slashed-zero;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.stacked-fractions{--tw-numeric-fraction:stacked-fractions;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.normal-nums{font-variant-numeric:normal}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_1px_hsl\(var\(--border\)\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,hsl(var(--border)));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.inset-ring{--tw-inset-ring-shadow:inset 0 0 0 1px var(--tw-inset-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-ring{--tw-ring-color:var(--ring)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a))drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a)drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)!important}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[left\,right\,width\]{transition-property:left,right,width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[margin\,opacity\]{transition-property:margin,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\,height\,padding\]{transition-property:width,height,padding;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.fade-in-0{--tw-enter-opacity:0}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.zoom-in-95{--tw-enter-scale:.95}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.paused{animation-play-state:paused}.ring-inset{--tw-ring-inset:inset}.running{animation-play-state:running}.zoom-in{--tw-enter-scale:0}.zoom-out{--tw-exit-scale:0}.group-focus-within\:opacity-0:is(:where(.group):focus-within *){opacity:0}.group-focus-within\/menu-item\:opacity-100:is(:where(.group\/menu-item):focus-within *){opacity:1}@media(hover:hover){.group-hover\:translate-x-0\.5:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:bg-accent:is(:where(.group):hover *){background-color:var(--accent)}.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--foreground)}.group-hover\:opacity-0:is(:where(.group):hover *){opacity:0}.group-hover\/menu-item\:opacity-100:is(:where(.group\/menu-item):hover *){opacity:1}}.group-has-\[\[data-sidebar\=menu-action\]\]\/menu-item\:pr-8:is(:where(.group\/menu-item):has([data-sidebar=menu-action]) *){padding-right:calc(var(--spacing)*8)}.group-has-\[\[data-slot\=item-description\]\]\/item\:translate-y-0\.5:is(:where(.group\/item):has([data-slot=item-description]) *){--tw-translate-y:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-has-\[\[data-slot\=item-description\]\]\/item\:self-start:is(:where(.group\/item):has([data-slot=item-description]) *){align-self:flex-start}.group-data-\[collapsible\=icon\]\:-mt-8:is(:where(.group)[data-collapsible=icon] *){margin-top:calc(var(--spacing)*-8)}.group-data-\[collapsible\=icon\]\:hidden:is(:where(.group)[data-collapsible=icon] *){display:none}.group-data-\[collapsible\=icon\]\:\!size-8:is(:where(.group)[data-collapsible=icon] *){width:calc(var(--spacing)*8)!important;height:calc(var(--spacing)*8)!important}.group-data-\[collapsible\=icon\]\:w-\[--sidebar-width-icon\]:is(:where(.group)[data-collapsible=icon] *){width:--sidebar-width-icon}.group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)\)\]:is(:where(.group)[data-collapsible=icon] *){width:calc(var(--sidebar-width-icon) + 1rem)}.group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)_\+2px\)\]:is(:where(.group)[data-collapsible=icon] *){width:calc(var(--sidebar-width-icon) + 1rem + 2px)}.group-data-\[collapsible\=icon\]\:overflow-hidden:is(:where(.group)[data-collapsible=icon] *){overflow:hidden}.group-data-\[collapsible\=icon\]\:\!p-0:is(:where(.group)[data-collapsible=icon] *){padding:calc(var(--spacing)*0)!important}.group-data-\[collapsible\=icon\]\:\!p-2:is(:where(.group)[data-collapsible=icon] *){padding:calc(var(--spacing)*2)!important}.group-data-\[collapsible\=icon\]\:opacity-0:is(:where(.group)[data-collapsible=icon] *){opacity:0}.group-data-\[collapsible\=offcanvas\]\:right-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.group)[data-collapsible=offcanvas] *){right:calc(var(--sidebar-width)*-1)}.group-data-\[collapsible\=offcanvas\]\:left-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.group)[data-collapsible=offcanvas] *){left:calc(var(--sidebar-width)*-1)}.group-data-\[collapsible\=offcanvas\]\:w-0:is(:where(.group)[data-collapsible=offcanvas] *){width:calc(var(--spacing)*0)}.group-data-\[collapsible\=offcanvas\]\:translate-x-0:is(:where(.group)[data-collapsible=offcanvas] *){--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-data-\[side\=left\]\:-right-4:is(:where(.group)[data-side=left] *){right:calc(var(--spacing)*-4)}.group-data-\[side\=left\]\:border-r:is(:where(.group)[data-side=left] *){border-right-style:var(--tw-border-style);border-right-width:1px}.group-data-\[side\=right\]\:left-0:is(:where(.group)[data-side=right] *){left:calc(var(--spacing)*0)}.group-data-\[side\=right\]\:rotate-180:is(:where(.group)[data-side=right] *){rotate:180deg}.group-data-\[side\=right\]\:border-l:is(:where(.group)[data-side=right] *){border-left-style:var(--tw-border-style);border-left-width:1px}.group-data-\[variant\=floating\]\:rounded-lg:is(:where(.group)[data-variant=floating] *){border-radius:var(--radius)}.group-data-\[variant\=floating\]\:border:is(:where(.group)[data-variant=floating] *){border-style:var(--tw-border-style);border-width:1px}.group-data-\[variant\=floating\]\:border-border:is(:where(.group)[data-variant=floating] *){border-color:var(--border)}.group-data-\[variant\=floating\]\:shadow:is(:where(.group)[data-variant=floating] *){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.peer-hover\/menu-button\:text-accent-foreground:is(:where(.peer\/menu-button):hover~*){color:var(--accent-foreground)}}.peer-data-\[active\=true\]\/menu-button\:text-accent-foreground:is(:where(.peer\/menu-button)[data-active=true]~*){color:var(--accent-foreground)}.peer-data-\[size\=default\]\/menu-button\:top-1\.5:is(:where(.peer\/menu-button)[data-size=default]~*){top:calc(var(--spacing)*1.5)}.peer-data-\[size\=lg\]\/menu-button\:top-2\.5:is(:where(.peer\/menu-button)[data-size=lg]~*){top:calc(var(--spacing)*2.5)}.peer-data-\[size\=sm\]\/menu-button\:top-1:is(:where(.peer\/menu-button)[data-size=sm]~*){top:calc(var(--spacing)*1)}.peer-data-\[state\=open\]\:opacity-0:is(:where(.peer)[data-state=open]~*){opacity:0}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-2:after{content:var(--tw-content);inset:calc(var(--spacing)*-2)}.after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--spacing)*0)}.after\:left-1\/2:after{content:var(--tw-content);left:50%}.after\:w-1:after{content:var(--tw-content);width:calc(var(--spacing)*1)}.after\:w-\[2px\]:after{content:var(--tw-content);width:2px}.after\:-translate-x-1\/2:after{content:var(--tw-content);--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.group-data-\[collapsible\=offcanvas\]\:after\:left-full:is(:where(.group)[data-collapsible=offcanvas] *):after{content:var(--tw-content);left:100%}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing)*-.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.hover\:border-ring:hover{border-color:var(--ring)}.hover\:bg-accent:hover,.hover\:bg-accent\/70:hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/70:hover{background-color:color-mix(in oklab,var(--accent)70%,transparent)}}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-500\/25:hover{background-color:#3080ff40}@supports (color:color-mix(in lab,red,red)){.hover\:bg-blue-500\/25:hover{background-color:color-mix(in oklab,var(--color-blue-500)25%,transparent)}}.hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.hover\:bg-red-100:hover{background-color:var(--color-red-100)}.hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:no-underline:hover{text-decoration-line:none}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-\[0_0_0_1px_hsl\(var\(--accent\)\)\]:hover{--tw-shadow:0 0 0 1px var(--tw-shadow-color,hsl(var(--accent)));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-\[collapsible\=offcanvas\]\:hover\:bg-background:is(:where(.group)[data-collapsible=offcanvas] *):hover{background-color:var(--background)}.hover\:after\:bg-border:hover:after{content:var(--tw-content);background-color:var(--border)}}.focus\:border-ring:focus{border-color:var(--ring)}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:text-destructive:focus{color:var(--destructive)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media(forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:z-10:focus-visible{z-index:10}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-0:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.focus-visible\:ring-offset-0:focus-visible{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:ring-offset-1:focus-visible{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color:var(--background)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-accent:active{background-color:var(--accent)}.active\:text-accent-foreground:active{color:var(--accent-foreground)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.has-data-\[variant\=inset\]\:bg-background:has([data-variant=inset]){background-color:var(--background)}.aria-disabled\:pointer-events-none[aria-disabled=true]{pointer-events:none}.aria-disabled\:opacity-50[aria-disabled=true]{opacity:.5}.data-\[active\=true\]\:bg-accent[data-active=true]{background-color:var(--accent)}.data-\[active\=true\]\:font-medium[data-active=true]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.data-\[active\=true\]\:text-accent-foreground[data-active=true]{color:var(--accent-foreground)}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[orientation\=horizontal\]\:h-px[data-orientation=horizontal]{height:1px}.data-\[orientation\=horizontal\]\:w-full[data-orientation=horizontal]{width:100%}.data-\[orientation\=vertical\]\:h-full[data-orientation=vertical]{height:100%}.data-\[orientation\=vertical\]\:w-px[data-orientation=vertical]{width:1px}.data-\[panel-group-direction\=vertical\]\:h-px[data-panel-group-direction=vertical]{height:1px}.data-\[panel-group-direction\=vertical\]\:w-full[data-panel-group-direction=vertical]{width:100%}.data-\[panel-group-direction\=vertical\]\:flex-col[data-panel-group-direction=vertical]{flex-direction:column}.data-\[panel-group-direction\=vertical\]\:after\:left-0[data-panel-group-direction=vertical]:after{content:var(--tw-content);left:calc(var(--spacing)*0)}.data-\[panel-group-direction\=vertical\]\:after\:h-1[data-panel-group-direction=vertical]:after{content:var(--tw-content);height:calc(var(--spacing)*1)}.data-\[panel-group-direction\=vertical\]\:after\:w-full[data-panel-group-direction=vertical]:after{content:var(--tw-content);width:100%}.data-\[panel-group-direction\=vertical\]\:after\:translate-x-0[data-panel-group-direction=vertical]:after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[panel-group-direction\=vertical\]\:after\:-translate-y-1\/2[data-panel-group-direction=vertical]:after{content:var(--tw-content);--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.data-\[state\=closed\]\:animate-accordion-up[data-state=closed]{animation:accordion-up var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:duration-300[data-state=closed]{--tw-duration:.3s;transition-duration:.3s}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y:100%}.data-\[state\=closed\]\:slide-out-to-bottom-4[data-state=closed]{--tw-exit-translate-y:calc(4*var(--spacing))}.data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x:-100%}.data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x:100%}.data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y:-100%}.data-\[state\=open\]\:animate-accordion-down[data-state=open]{animation:accordion-down var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.data-\[state\=open\]\:bg-secondary[data-state=open]{background-color:var(--secondary)}.data-\[state\=open\]\:opacity-100[data-state=open]{opacity:1}.data-\[state\=open\]\:duration-500[data-state=open]{--tw-duration:.5s;transition-duration:.5s}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y:100%}.data-\[state\=open\]\:slide-in-from-bottom-4[data-state=open]{--tw-enter-translate-y:calc(4*var(--spacing))}.data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x:-100%}.data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x:100%}.data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y:-100%}@media(hover:hover){.data-\[state\=open\]\:hover\:bg-accent[data-state=open]:hover{background-color:var(--accent)}.data-\[state\=open\]\:hover\:text-accent-foreground[data-state=open]:hover{color:var(--accent-foreground)}}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:var(--input)}@media(min-width:40rem){.sm\:mt-0{margin-top:calc(var(--spacing)*0)}.sm\:flex{display:flex}.sm\:max-w-sm{max-width:var(--container-sm)}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media(min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.md\:opacity-0{opacity:0}.md\:peer-data-\[variant\=inset\]\:m-2:is(:where(.peer)[data-variant=inset]~*){margin:calc(var(--spacing)*2)}.md\:peer-data-\[variant\=inset\]\:ml-0:is(:where(.peer)[data-variant=inset]~*){margin-left:calc(var(--spacing)*0)}.md\:peer-data-\[variant\=inset\]\:rounded-xl:is(:where(.peer)[data-variant=inset]~*){border-radius:calc(var(--radius) + 4px)}.md\:peer-data-\[variant\=inset\]\:shadow:is(:where(.peer)[data-variant=inset]~*){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:peer-data-\[state\=collapsed\]\:peer-data-\[variant\=inset\]\:ml-2:is(:where(.peer)[data-state=collapsed]~*):is(:where(.peer)[data-variant=inset]~*){margin-left:calc(var(--spacing)*2)}}.after\:md\:hidden:after{content:var(--tw-content)}@media(min-width:48rem){.after\:md\:hidden:after{display:none}}@media(hover:hover){.dark\:hover\:bg-red-900\/30:is(.dark *):hover{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-red-900\/30:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}}.\[\&_\.cm-content\]\:p-0 .cm-content{padding:calc(var(--spacing)*0)}.\[\&_\.cm-content\]\:px-0 .cm-content{padding-inline:calc(var(--spacing)*0)}.\[\&_\.cm-content\]\:py-0 .cm-content{padding-block:calc(var(--spacing)*0)}.\[\&_\.cm-content\]\:text-sm .cm-content{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\[\&_\.cm-content\]\:text-foreground .cm-content{color:var(--foreground)}.\[\&_\.cm-editor\]\:h-full .cm-editor{height:100%}.\[\&_\.cm-editor\]\:w-full .cm-editor{width:100%}.\[\&_\.cm-editor\]\:cursor-text .cm-editor{cursor:text}.\[\&_\.cm-editor\]\:rounded-none .cm-editor{border-radius:0}.\[\&_\.cm-editor\]\:border-0 .cm-editor{border-style:var(--tw-border-style);border-width:0}.\[\&_\.cm-editor\]\:bg-background .cm-editor{background-color:var(--background)}.\[\&_\.cm-editor\]\:bg-transparent .cm-editor{background-color:#0000}.\[\&_\.cm-editor\]\:p-0 .cm-editor{padding:calc(var(--spacing)*0)}.\[\&_\.cm-focused\]\:outline-none .cm-focused{--tw-outline-style:none;outline-style:none}.\[\&_\.cm-gutters\]\:bg-background .cm-gutters{background-color:var(--background)}.\[\&_\.cm-lineNumbers\]\:text-muted-foreground .cm-lineNumbers,.\[\&_\.cm-placeholder\]\:text-muted-foreground .cm-placeholder{color:var(--muted-foreground)}.\[\&_\.cm-scroller\]\:p-0 .cm-scroller{padding:calc(var(--spacing)*0)}.\[\&_\.cm-scroller\]\:font-mono .cm-scroller{font-family:var(--font-mono)}.\[\&_img\]\:size-full img{width:100%;height:100%}.\[\&_img\]\:object-cover img{object-fit:cover}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&\+\[data-slot\=item-content\]\]\:flex-none+[data-slot=item-content]{flex:none}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing)*0)}.\[\[role\=checkbox\]\]\:translate-y-\[2px\][role=checkbox]{--tw-translate-y:2px;translate:var(--tw-translate-x)var(--tw-translate-y)}.\[a\]\:transition-colors:is(a){transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.\[a\]\:hover\:bg-accent\/50:is(a):hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.\[a\]\:hover\:bg-accent\/50:is(a):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}}.\[\&\>a\]\:underline>a{text-decoration-line:underline}.\[\&\>a\]\:underline-offset-4>a{text-underline-offset:4px}.\[\&\>a\:hover\]\:text-primary>a:hover{color:var(--primary)}.\[\&\>button\]\:hidden>button{display:none}.\[\&\>span\:last-child\]\:truncate>span:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.\[\&\>svg\]\:ml-auto>svg{margin-left:auto}.\[\&\>svg\]\:size-4>svg{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.\[\&\>svg\]\:text-accent-foreground>svg{color:var(--accent-foreground)}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.\[\&\[data-panel-group-direction\=vertical\]\>div\]\:rotate-90[data-panel-group-direction=vertical]>div{rotate:90deg}.\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{rotate:180deg}[data-side=left] .\[\[data-side\=left\]_\&\]\:cursor-w-resize{cursor:w-resize}[data-side=left][data-collapsible=offcanvas] .\[\[data-side\=left\]\[data-collapsible\=offcanvas\]_\&\]\:-right-2{right:calc(var(--spacing)*-2)}[data-side=left][data-state=collapsed] .\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize,[data-side=right] .\[\[data-side\=right\]_\&\]\:cursor-e-resize{cursor:e-resize}[data-side=right][data-collapsible=offcanvas] .\[\[data-side\=right\]\[data-collapsible\=offcanvas\]_\&\]\:-left-2{left:calc(var(--spacing)*-2)}[data-side=right][data-state=collapsed] .\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize{cursor:w-resize}[data-slot=tooltip-content] .\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/20{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){[data-slot=tooltip-content] .\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/20{background-color:color-mix(in oklab,var(--background)20%,transparent)}}[data-slot=tooltip-content] .\[\[data-slot\=tooltip-content\]_\&\]\:text-background{color:var(--background)}[data-slot=tooltip-content] .dark\:\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/10:is(.dark *){background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){[data-slot=tooltip-content] .dark\:\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/10:is(.dark *){background-color:color-mix(in oklab,var(--background)10%,transparent)}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(15% .004 260);--card:oklch(98% 0 0);--card-foreground:oklch(15% .004 260);--popover:oklch(100% 0 0);--popover-foreground:oklch(15% .004 260);--primary:oklch(22% .005 260);--primary-foreground:oklch(98% 0 0);--secondary:oklch(96% .002 260);--secondary-foreground:oklch(22% .005 260);--muted:oklch(96% .002 260);--muted-foreground:oklch(50% .01 260);--accent:oklch(95% .002 260);--accent-foreground:oklch(22% .005 260);--destructive:oklch(55% .22 25);--destructive-foreground:oklch(98% 0 0);--border:oklch(90% .003 260);--input:oklch(92% .003 260);--ring:oklch(22% .005 260)}.dark{--background:oklch(10% .005 260);--foreground:oklch(95% .005 260);--card:oklch(14% .006 260);--card-foreground:oklch(95% .005 260);--popover:oklch(14% .006 260);--popover-foreground:oklch(95% .005 260);--primary:oklch(85% .008 260);--primary-foreground:oklch(14% .006 260);--secondary:oklch(20% .008 260);--secondary-foreground:oklch(95% .005 260);--muted:oklch(20% .008 260);--muted-foreground:oklch(65% .012 260);--accent:oklch(18% .008 260);--accent-foreground:oklch(95% .005 260);--destructive:oklch(60% .2 25);--destructive-foreground:oklch(98% 0 0);--border:oklch(25% .01 260);--input:oklch(20% .008 260);--ring:oklch(85% .008 260)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto)))))}}@keyframes accordion-up{0%{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto)))))}to{height:0}} diff --git a/packages/cli/src/db-studio/ui/dist/assets/index-xSDzOWv1.js b/packages/cli/src/db-studio/ui/dist/assets/index-xSDzOWv1.js new file mode 100644 index 0000000000..838dd12b69 --- /dev/null +++ b/packages/cli/src/db-studio/ui/dist/assets/index-xSDzOWv1.js @@ -0,0 +1,366 @@ +function r2(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();function CN(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var Hm={exports:{}},Af={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Av;function i2(){if(Av)return Af;Av=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,s){var a=null;if(s!==void 0&&(a=""+s),i.key!==void 0&&(a=""+i.key),"key"in i){s={};for(var c in i)c!=="key"&&(s[c]=i[c])}else s=i;return i=s.ref,{$$typeof:t,type:r,key:a,ref:i!==void 0?i:null,props:s}}return Af.Fragment=e,Af.jsx=n,Af.jsxs=n,Af}var gv;function s2(){return gv||(gv=1,Hm.exports=i2()),Hm.exports}var m=s2(),Fm={exports:{}},at={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ov;function o2(){if(Ov)return at;Ov=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),a=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),u=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),h=Symbol.for("react.lazy"),T=Symbol.for("react.activity"),p=Symbol.iterator;function R(x){return x===null||typeof x!="object"?null:(x=p&&x[p]||x["@@iterator"],typeof x=="function"?x:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},N=Object.assign,I={};function C(x,X,Y){this.props=x,this.context=X,this.refs=I,this.updater=Y||g}C.prototype.isReactComponent={},C.prototype.setState=function(x,X){if(typeof x!="object"&&typeof x!="function"&&x!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,x,X,"setState")},C.prototype.forceUpdate=function(x){this.updater.enqueueForceUpdate(this,x,"forceUpdate")};function L(){}L.prototype=C.prototype;function y(x,X,Y){this.props=x,this.context=X,this.refs=I,this.updater=Y||g}var v=y.prototype=new L;v.constructor=y,N(v,C.prototype),v.isPureReactComponent=!0;var b=Array.isArray;function D(){}var M={H:null,A:null,T:null,S:null},w=Object.prototype.hasOwnProperty;function F(x,X,Y){var re=Y.ref;return{$$typeof:t,type:x,key:X,ref:re!==void 0?re:null,props:Y}}function Q(x,X){return F(x.type,X,x.props)}function ne(x){return typeof x=="object"&&x!==null&&x.$$typeof===t}function z(x){var X={"=":"=0",":":"=2"};return"$"+x.replace(/[=:]/g,function(Y){return X[Y]})}var ie=/\/+/g;function se(x,X){return typeof x=="object"&&x!==null&&x.key!=null?z(""+x.key):X.toString(36)}function Ee(x){switch(x.status){case"fulfilled":return x.value;case"rejected":throw x.reason;default:switch(typeof x.status=="string"?x.then(D,D):(x.status="pending",x.then(function(X){x.status==="pending"&&(x.status="fulfilled",x.value=X)},function(X){x.status==="pending"&&(x.status="rejected",x.reason=X)})),x.status){case"fulfilled":return x.value;case"rejected":throw x.reason}}throw x}function U(x,X,Y,re,Re){var me=typeof x;(me==="undefined"||me==="boolean")&&(x=null);var q=!1;if(x===null)q=!0;else switch(me){case"bigint":case"string":case"number":q=!0;break;case"object":switch(x.$$typeof){case t:case e:q=!0;break;case h:return q=x._init,U(q(x._payload),X,Y,re,Re)}}if(q)return Re=Re(x),q=re===""?"."+se(x,0):re,b(Re)?(Y="",q!=null&&(Y=q.replace(ie,"$&/")+"/"),U(Re,X,Y,"",function(He){return He})):Re!=null&&(ne(Re)&&(Re=Q(Re,Y+(Re.key==null||x&&x.key===Re.key?"":(""+Re.key).replace(ie,"$&/")+"/")+q)),X.push(Re)),1;q=0;var Ce=re===""?".":re+":";if(b(x))for(var Le=0;Le>>1,ge=U[oe];if(0>>1;oei(Y,V))rei(Re,Y)?(U[oe]=Re,U[re]=V,oe=re):(U[oe]=Y,U[X]=V,oe=X);else if(rei(Re,V))U[oe]=Re,U[re]=V,oe=re;else break e}}return Z}function i(U,Z){var V=U.sortIndex-Z.sortIndex;return V!==0?V:U.id-Z.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var a=Date,c=a.now();t.unstable_now=function(){return a.now()-c}}var u=[],d=[],h=1,T=null,p=3,R=!1,g=!1,N=!1,I=!1,C=typeof setTimeout=="function"?setTimeout:null,L=typeof clearTimeout=="function"?clearTimeout:null,y=typeof setImmediate<"u"?setImmediate:null;function v(U){for(var Z=n(d);Z!==null;){if(Z.callback===null)r(d);else if(Z.startTime<=U)r(d),Z.sortIndex=Z.expirationTime,e(u,Z);else break;Z=n(d)}}function b(U){if(N=!1,v(U),!g)if(n(u)!==null)g=!0,D||(D=!0,z());else{var Z=n(d);Z!==null&&Ee(b,Z.startTime-U)}}var D=!1,M=-1,w=5,F=-1;function Q(){return I?!0:!(t.unstable_now()-FU&&Q());){var oe=T.callback;if(typeof oe=="function"){T.callback=null,p=T.priorityLevel;var ge=oe(T.expirationTime<=U);if(U=t.unstable_now(),typeof ge=="function"){T.callback=ge,v(U),Z=!0;break t}T===n(u)&&r(u),v(U)}else r(u);T=n(u)}if(T!==null)Z=!0;else{var x=n(d);x!==null&&Ee(b,x.startTime-U),Z=!1}}break e}finally{T=null,p=V,R=!1}Z=void 0}}finally{Z?z():D=!1}}}var z;if(typeof y=="function")z=function(){y(ne)};else if(typeof MessageChannel<"u"){var ie=new MessageChannel,se=ie.port2;ie.port1.onmessage=ne,z=function(){se.postMessage(null)}}else z=function(){C(ne,0)};function Ee(U,Z){M=C(function(){U(t.unstable_now())},Z)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(U){U.callback=null},t.unstable_forceFrameRate=function(U){0>U||125oe?(U.sortIndex=V,e(d,U),n(u)===null&&U===n(d)&&(N?(L(M),M=-1):N=!0,Ee(b,V-oe))):(U.sortIndex=ge,e(u,U),g||R||(g=!0,D||(D=!0,z()))),U},t.unstable_shouldYield=Q,t.unstable_wrapCallback=function(U){var Z=p;return function(){var V=p;p=Z;try{return U.apply(this,arguments)}finally{p=V}}}})(Ym)),Ym}var Cv;function l2(){return Cv||(Cv=1,Vm.exports=a2()),Vm.exports}var Wm={exports:{}},nr={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var _v;function c2(){if(_v)return nr;_v=1;var t=_N();function e(u){var d="https://react.dev/errors/"+u;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),Wm.exports=c2(),Wm.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var yv;function u2(){if(yv)return gf;yv=1;var t=l2(),e=_N(),n=pP();function r(o){var l="https://react.dev/errors/"+o;if(1ge||(o.current=oe[ge],oe[ge]=null,ge--)}function Y(o,l){ge++,oe[ge]=o.current,o.current=l}var re=x(null),Re=x(null),me=x(null),q=x(null);function Ce(o,l){switch(Y(me,l),Y(Re,o),Y(re,null),l.nodeType){case 9:case 11:o=(o=l.documentElement)&&(o=o.namespaceURI)?Yy(o):0;break;default:if(o=l.tagName,l=l.namespaceURI)l=Yy(l),o=Wy(l,o);else switch(o){case"svg":o=1;break;case"math":o=2;break;default:o=0}}X(re),Y(re,o)}function Le(){X(re),X(Re),X(me)}function He(o){o.memoizedState!==null&&Y(q,o);var l=re.current,f=Wy(l,o.type);l!==f&&(Y(Re,o),Y(re,f))}function ot(o){Re.current===o&&(X(re),X(Re)),q.current===o&&(X(q),pf._currentValue=V)}var it,Ze;function dt(o){if(it===void 0)try{throw Error()}catch(f){var l=f.stack.trim().match(/\n( *(at )?)/);it=l&&l[1]||"",Ze=-1)":-1S||B[E]!==J[S]){var fe=` +`+B[E].replace(" at new "," at ");return o.displayName&&fe.includes("")&&(fe=fe.replace("",o.displayName)),fe}while(1<=E&&0<=S);break}}}finally{Ct=!1,Error.prepareStackTrace=f}return(f=o?o.displayName||o.name:"")?dt(f):""}function lr(o,l){switch(o.tag){case 26:case 27:case 5:return dt(o.type);case 16:return dt("Lazy");case 13:return o.child!==l&&l!==null?dt("Suspense Fallback"):dt("Suspense");case 19:return dt("SuspenseList");case 0:case 15:return cn(o.type,!1);case 11:return cn(o.type.render,!1);case 1:return cn(o.type,!0);case 31:return dt("Activity");default:return""}}function Xt(o){try{var l="",f=null;do l+=lr(o,f),f=o,o=o.return;while(o);return l}catch(E){return` +Error generating stack: `+E.message+` +`+E.stack}}var cr=Object.prototype.hasOwnProperty,H=t.unstable_scheduleCallback,j=t.unstable_cancelCallback,ue=t.unstable_shouldYield,ve=t.unstable_requestPaint,Oe=t.unstable_now,Ie=t.unstable_getCurrentPriorityLevel,Ge=t.unstable_ImmediatePriority,lt=t.unstable_UserBlockingPriority,_t=t.unstable_NormalPriority,er=t.unstable_LowPriority,vi=t.unstable_IdlePriority,ei=t.log,Mr=t.unstable_setDisableYieldValue,ti=null,An=null;function Pr(o){if(typeof ei=="function"&&Mr(o),An&&typeof An.setStrictMode=="function")try{An.setStrictMode(ti,o)}catch{}}var Bn=Math.clz32?Math.clz32:eh,Bl=Math.log,xs=Math.LN2;function eh(o){return o>>>=0,o===0?32:31-(Bl(o)/xs|0)|0}var Lo=256,Gl=262144,ba=4194304;function ws(o){var l=o&42;if(l!==0)return l;switch(o&-o){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return o&261888;case 262144:case 524288:case 1048576:case 2097152:return o&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return o&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return o}}function Ma(o,l,f){var E=o.pendingLanes;if(E===0)return 0;var S=0,A=o.suspendedLanes,_=o.pingedLanes;o=o.warmLanes;var P=E&134217727;return P!==0?(E=P&~A,E!==0?S=ws(E):(_&=P,_!==0?S=ws(_):f||(f=P&~o,f!==0&&(S=ws(f))))):(P=E&~A,P!==0?S=ws(P):_!==0?S=ws(_):f||(f=E&~o,f!==0&&(S=ws(f)))),S===0?0:l!==0&&l!==S&&(l&A)===0&&(A=S&-S,f=l&-l,A>=f||A===32&&(f&4194048)!==0)?l:S}function qi(o,l){return(o.pendingLanes&~(o.suspendedLanes&~o.pingedLanes)&l)===0}function Hl(o,l){switch(o){case 1:case 2:case 4:case 8:case 64:return l+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return l+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function th(){var o=ba;return ba<<=1,(ba&62914560)===0&&(ba=4194304),o}function bu(o){for(var l=[],f=0;31>f;f++)l.push(o);return l}function G(o,l){o.pendingLanes|=l,l!==268435456&&(o.suspendedLanes=0,o.pingedLanes=0,o.warmLanes=0)}function $(o,l,f,E,S,A){var _=o.pendingLanes;o.pendingLanes=f,o.suspendedLanes=0,o.pingedLanes=0,o.warmLanes=0,o.expiredLanes&=f,o.entangledLanes&=f,o.errorRecoveryDisabledLanes&=f,o.shellSuspendCounter=0;var P=o.entanglements,B=o.expirationTimes,J=o.hiddenUpdates;for(f=_&~f;0"u")return null;try{return o.activeElement||o.body}catch{return o.body}}var Jk=/[\n"\\]/g;function si(o){return o.replace(Jk,function(l){return"\\"+l.charCodeAt(0).toString(16)+" "})}function PR(o,l,f,E,S,A,_,P){o.name="",_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"?o.type=_:o.removeAttribute("type"),l!=null?_==="number"?(l===0&&o.value===""||o.value!=l)&&(o.value=""+ii(l)):o.value!==""+ii(l)&&(o.value=""+ii(l)):_!=="submit"&&_!=="reset"||o.removeAttribute("value"),l!=null?xR(o,_,ii(l)):f!=null?xR(o,_,ii(f)):E!=null&&o.removeAttribute("value"),S==null&&A!=null&&(o.defaultChecked=!!A),S!=null&&(o.checked=S&&typeof S!="function"&&typeof S!="symbol"),P!=null&&typeof P!="function"&&typeof P!="symbol"&&typeof P!="boolean"?o.name=""+ii(P):o.removeAttribute("name")}function BC(o,l,f,E,S,A,_,P){if(A!=null&&typeof A!="function"&&typeof A!="symbol"&&typeof A!="boolean"&&(o.type=A),l!=null||f!=null){if(!(A!=="submit"&&A!=="reset"||l!=null)){MR(o);return}f=f!=null?""+ii(f):"",l=l!=null?""+ii(l):f,P||l===o.value||(o.value=l),o.defaultValue=l}E=E??S,E=typeof E!="function"&&typeof E!="symbol"&&!!E,o.checked=P?o.checked:!!E,o.defaultChecked=!!E,_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"&&(o.name=_),MR(o)}function xR(o,l,f){l==="number"&&nh(o.ownerDocument)===o||o.defaultValue===""+f||(o.defaultValue=""+f)}function Fl(o,l,f,E){if(o=o.options,l){l={};for(var S=0;S"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),HR=!1;if(Hs)try{var Pu={};Object.defineProperty(Pu,"passive",{get:function(){HR=!0}}),window.addEventListener("test",Pu,Pu),window.removeEventListener("test",Pu,Pu)}catch{HR=!1}var yo=null,FR=null,ih=null;function WC(){if(ih)return ih;var o,l=FR,f=l.length,E,S="value"in yo?yo.value:yo.textContent,A=S.length;for(o=0;o=Uu),qC=" ",JC=!1;function QC(o,l){switch(o){case"keyup":return CV.indexOf(l.keyCode)!==-1;case"keydown":return l.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ZC(o){return o=o.detail,typeof o=="object"&&"data"in o?o.data:null}var Wl=!1;function LV(o,l){switch(o){case"compositionend":return ZC(l);case"keypress":return l.which!==32?null:(JC=!0,qC);case"textInput":return o=l.data,o===qC&&JC?null:o;default:return null}}function yV(o,l){if(Wl)return o==="compositionend"||!$R&&QC(o,l)?(o=WC(),ih=FR=yo=null,Wl=!1,o):null;switch(o){case"paste":return null;case"keypress":if(!(l.ctrlKey||l.altKey||l.metaKey)||l.ctrlKey&&l.altKey){if(l.char&&1=l)return{node:f,offset:l-o};o=E}e:{for(;f;){if(f.nextSibling){f=f.nextSibling;break e}f=f.parentNode}f=void 0}f=a_(f)}}function c_(o,l){return o&&l?o===l?!0:o&&o.nodeType===3?!1:l&&l.nodeType===3?c_(o,l.parentNode):"contains"in o?o.contains(l):o.compareDocumentPosition?!!(o.compareDocumentPosition(l)&16):!1:!1}function u_(o){o=o!=null&&o.ownerDocument!=null&&o.ownerDocument.defaultView!=null?o.ownerDocument.defaultView:window;for(var l=nh(o.document);l instanceof o.HTMLIFrameElement;){try{var f=typeof l.contentWindow.location.href=="string"}catch{f=!1}if(f)o=l.contentWindow;else break;l=nh(o.document)}return l}function XR(o){var l=o&&o.nodeName&&o.nodeName.toLowerCase();return l&&(l==="input"&&(o.type==="text"||o.type==="search"||o.type==="tel"||o.type==="url"||o.type==="password")||l==="textarea"||o.contentEditable==="true")}var UV=Hs&&"documentMode"in document&&11>=document.documentMode,$l=null,KR=null,Fu=null,qR=!1;function f_(o,l,f){var E=f.window===f?f.document:f.nodeType===9?f:f.ownerDocument;qR||$l==null||$l!==nh(E)||(E=$l,"selectionStart"in E&&XR(E)?E={start:E.selectionStart,end:E.selectionEnd}:(E=(E.ownerDocument&&E.ownerDocument.defaultView||window).getSelection(),E={anchorNode:E.anchorNode,anchorOffset:E.anchorOffset,focusNode:E.focusNode,focusOffset:E.focusOffset}),Fu&&Hu(Fu,E)||(Fu=E,E=Jh(KR,"onSelect"),0>=_,S-=_,es=1<<32-Bn(l)+S|f<ft?(Ot=$e,$e=null):Ot=$e.sibling;var Mt=ee(W,$e,K[ft],he);if(Mt===null){$e===null&&($e=Ot);break}o&&$e&&Mt.alternate===null&&l(W,$e),k=A(Mt,k,ft),bt===null?Je=Mt:bt.sibling=Mt,bt=Mt,$e=Ot}if(ft===K.length)return f(W,$e),Nt&&ks(W,ft),Je;if($e===null){for(;ftft?(Ot=$e,$e=null):Ot=$e.sibling;var qo=ee(W,$e,Mt.value,he);if(qo===null){$e===null&&($e=Ot);break}o&&$e&&qo.alternate===null&&l(W,$e),k=A(qo,k,ft),bt===null?Je=qo:bt.sibling=qo,bt=qo,$e=Ot}if(Mt.done)return f(W,$e),Nt&&ks(W,ft),Je;if($e===null){for(;!Mt.done;ft++,Mt=K.next())Mt=Te(W,Mt.value,he),Mt!==null&&(k=A(Mt,k,ft),bt===null?Je=Mt:bt.sibling=Mt,bt=Mt);return Nt&&ks(W,ft),Je}for($e=E($e);!Mt.done;ft++,Mt=K.next())Mt=ce($e,W,ft,Mt.value,he),Mt!==null&&(o&&Mt.alternate!==null&&$e.delete(Mt.key===null?ft:Mt.key),k=A(Mt,k,ft),bt===null?Je=Mt:bt.sibling=Mt,bt=Mt);return o&&$e.forEach(function(n2){return l(W,n2)}),Nt&&ks(W,ft),Je}function Vt(W,k,K,he){if(typeof K=="object"&&K!==null&&K.type===N&&K.key===null&&(K=K.props.children),typeof K=="object"&&K!==null){switch(K.$$typeof){case R:e:{for(var Je=K.key;k!==null;){if(k.key===Je){if(Je=K.type,Je===N){if(k.tag===7){f(W,k.sibling),he=S(k,K.props.children),he.return=W,W=he;break e}}else if(k.elementType===Je||typeof Je=="object"&&Je!==null&&Je.$$typeof===w&&Wa(Je)===k.type){f(W,k.sibling),he=S(k,K.props),zu(he,K),he.return=W,W=he;break e}f(W,k);break}else l(W,k);k=k.sibling}K.type===N?(he=Ha(K.props.children,W.mode,he,K.key),he.return=W,W=he):(he=Eh(K.type,K.key,K.props,null,W.mode,he),zu(he,K),he.return=W,W=he)}return _(W);case g:e:{for(Je=K.key;k!==null;){if(k.key===Je)if(k.tag===4&&k.stateNode.containerInfo===K.containerInfo&&k.stateNode.implementation===K.implementation){f(W,k.sibling),he=S(k,K.children||[]),he.return=W,W=he;break e}else{f(W,k);break}else l(W,k);k=k.sibling}he=rS(K,W.mode,he),he.return=W,W=he}return _(W);case w:return K=Wa(K),Vt(W,k,K,he)}if(Ee(K))return Fe(W,k,K,he);if(z(K)){if(Je=z(K),typeof Je!="function")throw Error(r(150));return K=Je.call(K),et(W,k,K,he)}if(typeof K.then=="function")return Vt(W,k,gh(K),he);if(K.$$typeof===y)return Vt(W,k,Rh(W,K),he);Oh(W,K)}return typeof K=="string"&&K!==""||typeof K=="number"||typeof K=="bigint"?(K=""+K,k!==null&&k.tag===6?(f(W,k.sibling),he=S(k,K),he.return=W,W=he):(f(W,k),he=nS(K,W.mode,he),he.return=W,W=he),_(W)):f(W,k)}return function(W,k,K,he){try{$u=0;var Je=Vt(W,k,K,he);return nc=null,Je}catch($e){if($e===tc||$e===mh)throw $e;var bt=wr(29,$e,null,W.mode);return bt.lanes=he,bt.return=W,bt}finally{}}}var za=x_(!0),w_=x_(!1),Po=!1;function TS(o){o.updateQueue={baseState:o.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function pS(o,l){o=o.updateQueue,l.updateQueue===o&&(l.updateQueue={baseState:o.baseState,firstBaseUpdate:o.firstBaseUpdate,lastBaseUpdate:o.lastBaseUpdate,shared:o.shared,callbacks:null})}function xo(o){return{lane:o,tag:0,payload:null,callback:null,next:null}}function wo(o,l,f){var E=o.updateQueue;if(E===null)return null;if(E=E.shared,(xt&2)!==0){var S=E.pending;return S===null?l.next=l:(l.next=S.next,S.next=l),E.pending=l,l=hh(o),S_(o,null,f),l}return dh(o,E,l,f),hh(o)}function ju(o,l,f){if(l=l.updateQueue,l!==null&&(l=l.shared,(f&4194048)!==0)){var E=l.lanes;E&=o.pendingLanes,f|=E,l.lanes=f,Ne(o,f)}}function RS(o,l){var f=o.updateQueue,E=o.alternate;if(E!==null&&(E=E.updateQueue,f===E)){var S=null,A=null;if(f=f.firstBaseUpdate,f!==null){do{var _={lane:f.lane,tag:f.tag,payload:f.payload,callback:null,next:null};A===null?S=A=_:A=A.next=_,f=f.next}while(f!==null);A===null?S=A=l:A=A.next=l}else S=A=l;f={baseState:E.baseState,firstBaseUpdate:S,lastBaseUpdate:A,shared:E.shared,callbacks:E.callbacks},o.updateQueue=f;return}o=f.lastBaseUpdate,o===null?f.firstBaseUpdate=l:o.next=l,f.lastBaseUpdate=l}var SS=!1;function Xu(){if(SS){var o=ec;if(o!==null)throw o}}function Ku(o,l,f,E){SS=!1;var S=o.updateQueue;Po=!1;var A=S.firstBaseUpdate,_=S.lastBaseUpdate,P=S.shared.pending;if(P!==null){S.shared.pending=null;var B=P,J=B.next;B.next=null,_===null?A=J:_.next=J,_=B;var fe=o.alternate;fe!==null&&(fe=fe.updateQueue,P=fe.lastBaseUpdate,P!==_&&(P===null?fe.firstBaseUpdate=J:P.next=J,fe.lastBaseUpdate=B))}if(A!==null){var Te=S.baseState;_=0,fe=J=B=null,P=A;do{var ee=P.lane&-536870913,ce=ee!==P.lane;if(ce?(gt&ee)===ee:(E&ee)===ee){ee!==0&&ee===Zl&&(SS=!0),fe!==null&&(fe=fe.next={lane:0,tag:P.tag,payload:P.payload,callback:null,next:null});e:{var Fe=o,et=P;ee=l;var Vt=f;switch(et.tag){case 1:if(Fe=et.payload,typeof Fe=="function"){Te=Fe.call(Vt,Te,ee);break e}Te=Fe;break e;case 3:Fe.flags=Fe.flags&-65537|128;case 0:if(Fe=et.payload,ee=typeof Fe=="function"?Fe.call(Vt,Te,ee):Fe,ee==null)break e;Te=T({},Te,ee);break e;case 2:Po=!0}}ee=P.callback,ee!==null&&(o.flags|=64,ce&&(o.flags|=8192),ce=S.callbacks,ce===null?S.callbacks=[ee]:ce.push(ee))}else ce={lane:ee,tag:P.tag,payload:P.payload,callback:P.callback,next:null},fe===null?(J=fe=ce,B=Te):fe=fe.next=ce,_|=ee;if(P=P.next,P===null){if(P=S.shared.pending,P===null)break;ce=P,P=ce.next,ce.next=null,S.lastBaseUpdate=ce,S.shared.pending=null}}while(!0);fe===null&&(B=Te),S.baseState=B,S.firstBaseUpdate=J,S.lastBaseUpdate=fe,A===null&&(S.shared.lanes=0),Fo|=_,o.lanes=_,o.memoizedState=Te}}function U_(o,l){if(typeof o!="function")throw Error(r(191,o));o.call(l)}function B_(o,l){var f=o.callbacks;if(f!==null)for(o.callbacks=null,o=0;oA?A:8;var _=U.T,P={};U.T=P,BS(o,!1,l,f);try{var B=S(),J=U.S;if(J!==null&&J(P,B),B!==null&&typeof B=="object"&&typeof B.then=="function"){var fe=$V(B,E);Qu(o,l,fe,Fr(o))}else Qu(o,l,E,Fr(o))}catch(Te){Qu(o,l,{then:function(){},status:"rejected",reason:Te},Fr())}finally{Z.p=A,_!==null&&P.types!==null&&(_.types=P.types),U.T=_}}function JV(){}function wS(o,l,f,E){if(o.tag!==5)throw Error(r(476));var S=TL(o).queue;EL(o,S,l,V,f===null?JV:function(){return pL(o),f(E)})}function TL(o){var l=o.memoizedState;if(l!==null)return l;l={memoizedState:V,baseState:V,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:$s,lastRenderedState:V},next:null};var f={};return l.next={memoizedState:f,baseState:f,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:$s,lastRenderedState:f},next:null},o.memoizedState=l,o=o.alternate,o!==null&&(o.memoizedState=l),l}function pL(o){var l=TL(o);l.next===null&&(l=o.alternate.memoizedState),Qu(o,l.next.queue,{},Fr())}function US(){return jn(pf)}function RL(){return Tn().memoizedState}function SL(){return Tn().memoizedState}function QV(o){for(var l=o.return;l!==null;){switch(l.tag){case 24:case 3:var f=Fr();o=xo(f);var E=wo(l,o,f);E!==null&&(Nr(E,l,f),ju(E,l,f)),l={cache:fS()},o.payload=l;return}l=l.return}}function ZV(o,l,f){var E=Fr();f={lane:E,revertLane:0,gesture:null,action:f,hasEagerState:!1,eagerState:null,next:null},Mh(o)?AL(l,f):(f=eS(o,l,f,E),f!==null&&(Nr(f,o,E),gL(f,l,E)))}function mL(o,l,f){var E=Fr();Qu(o,l,f,E)}function Qu(o,l,f,E){var S={lane:E,revertLane:0,gesture:null,action:f,hasEagerState:!1,eagerState:null,next:null};if(Mh(o))AL(l,S);else{var A=o.alternate;if(o.lanes===0&&(A===null||A.lanes===0)&&(A=l.lastRenderedReducer,A!==null))try{var _=l.lastRenderedState,P=A(_,f);if(S.hasEagerState=!0,S.eagerState=P,xr(P,_))return dh(o,l,S,0),$t===null&&fh(),!1}catch{}finally{}if(f=eS(o,l,S,E),f!==null)return Nr(f,o,E),gL(f,l,E),!0}return!1}function BS(o,l,f,E){if(E={lane:2,revertLane:Tm(),gesture:null,action:E,hasEagerState:!1,eagerState:null,next:null},Mh(o)){if(l)throw Error(r(479))}else l=eS(o,f,E,2),l!==null&&Nr(l,o,2)}function Mh(o){var l=o.alternate;return o===ct||l!==null&&l===ct}function AL(o,l){ic=Ch=!0;var f=o.pending;f===null?l.next=l:(l.next=f.next,f.next=l),o.pending=l}function gL(o,l,f){if((f&4194048)!==0){var E=l.lanes;E&=o.pendingLanes,f|=E,l.lanes=f,Ne(o,f)}}var Zu={readContext:jn,use:yh,useCallback:un,useContext:un,useEffect:un,useImperativeHandle:un,useLayoutEffect:un,useInsertionEffect:un,useMemo:un,useReducer:un,useRef:un,useState:un,useDebugValue:un,useDeferredValue:un,useTransition:un,useSyncExternalStore:un,useId:un,useHostTransitionStatus:un,useFormState:un,useActionState:un,useOptimistic:un,useMemoCache:un,useCacheRefresh:un};Zu.useEffectEvent=un;var OL={readContext:jn,use:yh,useCallback:function(o,l){return fr().memoizedState=[o,l===void 0?null:l],o},useContext:jn,useEffect:sL,useImperativeHandle:function(o,l,f){f=f!=null?f.concat([o]):null,Dh(4194308,4,cL.bind(null,l,o),f)},useLayoutEffect:function(o,l){return Dh(4194308,4,o,l)},useInsertionEffect:function(o,l){Dh(4,2,o,l)},useMemo:function(o,l){var f=fr();l=l===void 0?null:l;var E=o();if(ja){Pr(!0);try{o()}finally{Pr(!1)}}return f.memoizedState=[E,l],E},useReducer:function(o,l,f){var E=fr();if(f!==void 0){var S=f(l);if(ja){Pr(!0);try{f(l)}finally{Pr(!1)}}}else S=l;return E.memoizedState=E.baseState=S,o={pending:null,lanes:0,dispatch:null,lastRenderedReducer:o,lastRenderedState:S},E.queue=o,o=o.dispatch=ZV.bind(null,ct,o),[E.memoizedState,o]},useRef:function(o){var l=fr();return o={current:o},l.memoizedState=o},useState:function(o){o=DS(o);var l=o.queue,f=mL.bind(null,ct,l);return l.dispatch=f,[o.memoizedState,f]},useDebugValue:PS,useDeferredValue:function(o,l){var f=fr();return xS(f,o,l)},useTransition:function(){var o=DS(!1);return o=EL.bind(null,ct,o.queue,!0,!1),fr().memoizedState=o,[!1,o]},useSyncExternalStore:function(o,l,f){var E=ct,S=fr();if(Nt){if(f===void 0)throw Error(r(407));f=f()}else{if(f=l(),$t===null)throw Error(r(349));(gt&127)!==0||Y_(E,l,f)}S.memoizedState=f;var A={value:f,getSnapshot:l};return S.queue=A,sL($_.bind(null,E,A,o),[o]),E.flags|=2048,oc(9,{destroy:void 0},W_.bind(null,E,A,f,l),null),f},useId:function(){var o=fr(),l=$t.identifierPrefix;if(Nt){var f=ts,E=es;f=(E&~(1<<32-Bn(E)-1)).toString(32)+f,l="_"+l+"R_"+f,f=_h++,0<\/script>",A=A.removeChild(A.firstChild);break;case"select":A=typeof E.is=="string"?_.createElement("select",{is:E.is}):_.createElement("select"),E.multiple?A.multiple=!0:E.size&&(A.size=E.size);break;default:A=typeof E.is=="string"?_.createElement(S,{is:E.is}):_.createElement(S)}}A[we]=l,A[Ue]=E;e:for(_=l.child;_!==null;){if(_.tag===5||_.tag===6)A.appendChild(_.stateNode);else if(_.tag!==4&&_.tag!==27&&_.child!==null){_.child.return=_,_=_.child;continue}if(_===l)break e;for(;_.sibling===null;){if(_.return===null||_.return===l)break e;_=_.return}_.sibling.return=_.return,_=_.sibling}l.stateNode=A;e:switch(Kn(A,S,E),S){case"button":case"input":case"select":case"textarea":E=!!E.autoFocus;break e;case"img":E=!0;break e;default:E=!1}E&&js(l)}}return Zt(l),JS(l,l.type,o===null?null:o.memoizedProps,l.pendingProps,f),null;case 6:if(o&&l.stateNode!=null)o.memoizedProps!==E&&js(l);else{if(typeof E!="string"&&l.stateNode===null)throw Error(r(166));if(o=me.current,Jl(l)){if(o=l.stateNode,f=l.memoizedProps,E=null,S=zn,S!==null)switch(S.tag){case 27:case 5:E=S.memoizedProps}o[we]=l,o=!!(o.nodeValue===f||E!==null&&E.suppressHydrationWarning===!0||ky(o.nodeValue,f)),o||bo(l,!0)}else o=Qh(o).createTextNode(E),o[we]=l,l.stateNode=o}return Zt(l),null;case 31:if(f=l.memoizedState,o===null||o.memoizedState!==null){if(E=Jl(l),f!==null){if(o===null){if(!E)throw Error(r(318));if(o=l.memoizedState,o=o!==null?o.dehydrated:null,!o)throw Error(r(557));o[we]=l}else Fa(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;Zt(l),o=!1}else f=aS(),o!==null&&o.memoizedState!==null&&(o.memoizedState.hydrationErrors=f),o=!0;if(!o)return l.flags&256?(Br(l),l):(Br(l),null);if((l.flags&128)!==0)throw Error(r(558))}return Zt(l),null;case 13:if(E=l.memoizedState,o===null||o.memoizedState!==null&&o.memoizedState.dehydrated!==null){if(S=Jl(l),E!==null&&E.dehydrated!==null){if(o===null){if(!S)throw Error(r(318));if(S=l.memoizedState,S=S!==null?S.dehydrated:null,!S)throw Error(r(317));S[we]=l}else Fa(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;Zt(l),S=!1}else S=aS(),o!==null&&o.memoizedState!==null&&(o.memoizedState.hydrationErrors=S),S=!0;if(!S)return l.flags&256?(Br(l),l):(Br(l),null)}return Br(l),(l.flags&128)!==0?(l.lanes=f,l):(f=E!==null,o=o!==null&&o.memoizedState!==null,f&&(E=l.child,S=null,E.alternate!==null&&E.alternate.memoizedState!==null&&E.alternate.memoizedState.cachePool!==null&&(S=E.alternate.memoizedState.cachePool.pool),A=null,E.memoizedState!==null&&E.memoizedState.cachePool!==null&&(A=E.memoizedState.cachePool.pool),A!==S&&(E.flags|=2048)),f!==o&&f&&(l.child.flags|=8192),Bh(l,l.updateQueue),Zt(l),null);case 4:return Le(),o===null&&mm(l.stateNode.containerInfo),Zt(l),null;case 10:return Ys(l.type),Zt(l),null;case 19:if(X(En),E=l.memoizedState,E===null)return Zt(l),null;if(S=(l.flags&128)!==0,A=E.rendering,A===null)if(S)tf(E,!1);else{if(fn!==0||o!==null&&(o.flags&128)!==0)for(o=l.child;o!==null;){if(A=Ih(o),A!==null){for(l.flags|=128,tf(E,!1),o=A.updateQueue,l.updateQueue=o,Bh(l,o),l.subtreeFlags=0,o=f,f=l.child;f!==null;)m_(f,o),f=f.sibling;return Y(En,En.current&1|2),Nt&&ks(l,E.treeForkCount),l.child}o=o.sibling}E.tail!==null&&Oe()>Vh&&(l.flags|=128,S=!0,tf(E,!1),l.lanes=4194304)}else{if(!S)if(o=Ih(A),o!==null){if(l.flags|=128,S=!0,o=o.updateQueue,l.updateQueue=o,Bh(l,o),tf(E,!0),E.tail===null&&E.tailMode==="hidden"&&!A.alternate&&!Nt)return Zt(l),null}else 2*Oe()-E.renderingStartTime>Vh&&f!==536870912&&(l.flags|=128,S=!0,tf(E,!1),l.lanes=4194304);E.isBackwards?(A.sibling=l.child,l.child=A):(o=E.last,o!==null?o.sibling=A:l.child=A,E.last=A)}return E.tail!==null?(o=E.tail,E.rendering=o,E.tail=o.sibling,E.renderingStartTime=Oe(),o.sibling=null,f=En.current,Y(En,S?f&1|2:f&1),Nt&&ks(l,E.treeForkCount),o):(Zt(l),null);case 22:case 23:return Br(l),AS(),E=l.memoizedState!==null,o!==null?o.memoizedState!==null!==E&&(l.flags|=8192):E&&(l.flags|=8192),E?(f&536870912)!==0&&(l.flags&128)===0&&(Zt(l),l.subtreeFlags&6&&(l.flags|=8192)):Zt(l),f=l.updateQueue,f!==null&&Bh(l,f.retryQueue),f=null,o!==null&&o.memoizedState!==null&&o.memoizedState.cachePool!==null&&(f=o.memoizedState.cachePool.pool),E=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(E=l.memoizedState.cachePool.pool),E!==f&&(l.flags|=2048),o!==null&&X(Ya),null;case 24:return f=null,o!==null&&(f=o.memoizedState.cache),l.memoizedState.cache!==f&&(l.flags|=2048),Ys(On),Zt(l),null;case 25:return null;case 30:return null}throw Error(r(156,l.tag))}function iY(o,l){switch(sS(l),l.tag){case 1:return o=l.flags,o&65536?(l.flags=o&-65537|128,l):null;case 3:return Ys(On),Le(),o=l.flags,(o&65536)!==0&&(o&128)===0?(l.flags=o&-65537|128,l):null;case 26:case 27:case 5:return ot(l),null;case 31:if(l.memoizedState!==null){if(Br(l),l.alternate===null)throw Error(r(340));Fa()}return o=l.flags,o&65536?(l.flags=o&-65537|128,l):null;case 13:if(Br(l),o=l.memoizedState,o!==null&&o.dehydrated!==null){if(l.alternate===null)throw Error(r(340));Fa()}return o=l.flags,o&65536?(l.flags=o&-65537|128,l):null;case 19:return X(En),null;case 4:return Le(),null;case 10:return Ys(l.type),null;case 22:case 23:return Br(l),AS(),o!==null&&X(Ya),o=l.flags,o&65536?(l.flags=o&-65537|128,l):null;case 24:return Ys(On),null;case 25:return null;default:return null}}function zL(o,l){switch(sS(l),l.tag){case 3:Ys(On),Le();break;case 26:case 27:case 5:ot(l);break;case 4:Le();break;case 31:l.memoizedState!==null&&Br(l);break;case 13:Br(l);break;case 19:X(En);break;case 10:Ys(l.type);break;case 22:case 23:Br(l),AS(),o!==null&&X(Ya);break;case 24:Ys(On)}}function nf(o,l){try{var f=l.updateQueue,E=f!==null?f.lastEffect:null;if(E!==null){var S=E.next;f=S;do{if((f.tag&o)===o){E=void 0;var A=f.create,_=f.inst;E=A(),_.destroy=E}f=f.next}while(f!==S)}}catch(P){Bt(l,l.return,P)}}function Go(o,l,f){try{var E=l.updateQueue,S=E!==null?E.lastEffect:null;if(S!==null){var A=S.next;E=A;do{if((E.tag&o)===o){var _=E.inst,P=_.destroy;if(P!==void 0){_.destroy=void 0,S=l;var B=f,J=P;try{J()}catch(fe){Bt(S,B,fe)}}}E=E.next}while(E!==A)}}catch(fe){Bt(l,l.return,fe)}}function jL(o){var l=o.updateQueue;if(l!==null){var f=o.stateNode;try{B_(l,f)}catch(E){Bt(o,o.return,E)}}}function XL(o,l,f){f.props=Xa(o.type,o.memoizedProps),f.state=o.memoizedState;try{f.componentWillUnmount()}catch(E){Bt(o,l,E)}}function rf(o,l){try{var f=o.ref;if(f!==null){switch(o.tag){case 26:case 27:case 5:var E=o.stateNode;break;case 30:E=o.stateNode;break;default:E=o.stateNode}typeof f=="function"?o.refCleanup=f(E):f.current=E}}catch(S){Bt(o,l,S)}}function ns(o,l){var f=o.ref,E=o.refCleanup;if(f!==null)if(typeof E=="function")try{E()}catch(S){Bt(o,l,S)}finally{o.refCleanup=null,o=o.alternate,o!=null&&(o.refCleanup=null)}else if(typeof f=="function")try{f(null)}catch(S){Bt(o,l,S)}else f.current=null}function KL(o){var l=o.type,f=o.memoizedProps,E=o.stateNode;try{e:switch(l){case"button":case"input":case"select":case"textarea":f.autoFocus&&E.focus();break e;case"img":f.src?E.src=f.src:f.srcSet&&(E.srcset=f.srcSet)}}catch(S){Bt(o,o.return,S)}}function QS(o,l,f){try{var E=o.stateNode;_Y(E,o.type,f,l),E[Ue]=l}catch(S){Bt(o,o.return,S)}}function qL(o){return o.tag===5||o.tag===3||o.tag===26||o.tag===27&&$o(o.type)||o.tag===4}function ZS(o){e:for(;;){for(;o.sibling===null;){if(o.return===null||qL(o.return))return null;o=o.return}for(o.sibling.return=o.return,o=o.sibling;o.tag!==5&&o.tag!==6&&o.tag!==18;){if(o.tag===27&&$o(o.type)||o.flags&2||o.child===null||o.tag===4)continue e;o.child.return=o,o=o.child}if(!(o.flags&2))return o.stateNode}}function em(o,l,f){var E=o.tag;if(E===5||E===6)o=o.stateNode,l?(f.nodeType===9?f.body:f.nodeName==="HTML"?f.ownerDocument.body:f).insertBefore(o,l):(l=f.nodeType===9?f.body:f.nodeName==="HTML"?f.ownerDocument.body:f,l.appendChild(o),f=f._reactRootContainer,f!=null||l.onclick!==null||(l.onclick=Gs));else if(E!==4&&(E===27&&$o(o.type)&&(f=o.stateNode,l=null),o=o.child,o!==null))for(em(o,l,f),o=o.sibling;o!==null;)em(o,l,f),o=o.sibling}function Gh(o,l,f){var E=o.tag;if(E===5||E===6)o=o.stateNode,l?f.insertBefore(o,l):f.appendChild(o);else if(E!==4&&(E===27&&$o(o.type)&&(f=o.stateNode),o=o.child,o!==null))for(Gh(o,l,f),o=o.sibling;o!==null;)Gh(o,l,f),o=o.sibling}function JL(o){var l=o.stateNode,f=o.memoizedProps;try{for(var E=o.type,S=l.attributes;S.length;)l.removeAttributeNode(S[0]);Kn(l,E,f),l[we]=o,l[Ue]=f}catch(A){Bt(o,o.return,A)}}var Xs=!1,Cn=!1,tm=!1,QL=typeof WeakSet=="function"?WeakSet:Set,Hn=null;function sY(o,l){if(o=o.containerInfo,Om=sE,o=u_(o),XR(o)){if("selectionStart"in o)var f={start:o.selectionStart,end:o.selectionEnd};else e:{f=(f=o.ownerDocument)&&f.defaultView||window;var E=f.getSelection&&f.getSelection();if(E&&E.rangeCount!==0){f=E.anchorNode;var S=E.anchorOffset,A=E.focusNode;E=E.focusOffset;try{f.nodeType,A.nodeType}catch{f=null;break e}var _=0,P=-1,B=-1,J=0,fe=0,Te=o,ee=null;t:for(;;){for(var ce;Te!==f||S!==0&&Te.nodeType!==3||(P=_+S),Te!==A||E!==0&&Te.nodeType!==3||(B=_+E),Te.nodeType===3&&(_+=Te.nodeValue.length),(ce=Te.firstChild)!==null;)ee=Te,Te=ce;for(;;){if(Te===o)break t;if(ee===f&&++J===S&&(P=_),ee===A&&++fe===E&&(B=_),(ce=Te.nextSibling)!==null)break;Te=ee,ee=Te.parentNode}Te=ce}f=P===-1||B===-1?null:{start:P,end:B}}else f=null}f=f||{start:0,end:0}}else f=null;for(Nm={focusedElem:o,selectionRange:f},sE=!1,Hn=l;Hn!==null;)if(l=Hn,o=l.child,(l.subtreeFlags&1028)!==0&&o!==null)o.return=l,Hn=o;else for(;Hn!==null;){switch(l=Hn,A=l.alternate,o=l.flags,l.tag){case 0:if((o&4)!==0&&(o=l.updateQueue,o=o!==null?o.events:null,o!==null))for(f=0;f title"))),Kn(A,E,f),A[we]=o,rn(A),E=A;break e;case"link":var _=iv("link","href",S).get(E+(f.href||""));if(_){for(var P=0;P<_.length;P++)if(A=_[P],A.getAttribute("href")===(f.href==null||f.href===""?null:f.href)&&A.getAttribute("rel")===(f.rel==null?null:f.rel)&&A.getAttribute("title")===(f.title==null?null:f.title)&&A.getAttribute("crossorigin")===(f.crossOrigin==null?null:f.crossOrigin)){_.splice(P,1);break t}}A=S.createElement(E),Kn(A,E,f),S.head.appendChild(A);break;case"meta":if(_=iv("meta","content",S).get(E+(f.content||""))){for(P=0;P<_.length;P++)if(A=_[P],A.getAttribute("content")===(f.content==null?null:""+f.content)&&A.getAttribute("name")===(f.name==null?null:f.name)&&A.getAttribute("property")===(f.property==null?null:f.property)&&A.getAttribute("http-equiv")===(f.httpEquiv==null?null:f.httpEquiv)&&A.getAttribute("charset")===(f.charSet==null?null:f.charSet)){_.splice(P,1);break t}}A=S.createElement(E),Kn(A,E,f),S.head.appendChild(A);break;default:throw Error(r(468,E))}A[we]=o,rn(A),E=A}o.stateNode=E}else sv(S,o.type,o.stateNode);else o.stateNode=rv(S,E,o.memoizedProps);else A!==E?(A===null?f.stateNode!==null&&(f=f.stateNode,f.parentNode.removeChild(f)):A.count--,E===null?sv(S,o.type,o.stateNode):rv(S,E,o.memoizedProps)):E===null&&o.stateNode!==null&&QS(o,o.memoizedProps,f.memoizedProps)}break;case 27:Ar(l,o),gr(o),E&512&&(Cn||f===null||ns(f,f.return)),f!==null&&E&4&&QS(o,o.memoizedProps,f.memoizedProps);break;case 5:if(Ar(l,o),gr(o),E&512&&(Cn||f===null||ns(f,f.return)),o.flags&32){S=o.stateNode;try{kl(S,"")}catch(Fe){Bt(o,o.return,Fe)}}E&4&&o.stateNode!=null&&(S=o.memoizedProps,QS(o,S,f!==null?f.memoizedProps:S)),E&1024&&(tm=!0);break;case 6:if(Ar(l,o),gr(o),E&4){if(o.stateNode===null)throw Error(r(162));E=o.memoizedProps,f=o.stateNode;try{f.nodeValue=E}catch(Fe){Bt(o,o.return,Fe)}}break;case 3:if(tE=null,S=Pi,Pi=Zh(l.containerInfo),Ar(l,o),Pi=S,gr(o),E&4&&f!==null&&f.memoizedState.isDehydrated)try{Sc(l.containerInfo)}catch(Fe){Bt(o,o.return,Fe)}tm&&(tm=!1,sy(o));break;case 4:E=Pi,Pi=Zh(o.stateNode.containerInfo),Ar(l,o),gr(o),Pi=E;break;case 12:Ar(l,o),gr(o);break;case 31:Ar(l,o),gr(o),E&4&&(E=o.updateQueue,E!==null&&(o.updateQueue=null,Hh(o,E)));break;case 13:Ar(l,o),gr(o),o.child.flags&8192&&o.memoizedState!==null!=(f!==null&&f.memoizedState!==null)&&(kh=Oe()),E&4&&(E=o.updateQueue,E!==null&&(o.updateQueue=null,Hh(o,E)));break;case 22:S=o.memoizedState!==null;var B=f!==null&&f.memoizedState!==null,J=Xs,fe=Cn;if(Xs=J||S,Cn=fe||B,Ar(l,o),Cn=fe,Xs=J,gr(o),E&8192)e:for(l=o.stateNode,l._visibility=S?l._visibility&-2:l._visibility|1,S&&(f===null||B||Xs||Cn||Ka(o)),f=null,l=o;;){if(l.tag===5||l.tag===26){if(f===null){B=f=l;try{if(A=B.stateNode,S)_=A.style,typeof _.setProperty=="function"?_.setProperty("display","none","important"):_.display="none";else{P=B.stateNode;var Te=B.memoizedProps.style,ee=Te!=null&&Te.hasOwnProperty("display")?Te.display:null;P.style.display=ee==null||typeof ee=="boolean"?"":(""+ee).trim()}}catch(Fe){Bt(B,B.return,Fe)}}}else if(l.tag===6){if(f===null){B=l;try{B.stateNode.nodeValue=S?"":B.memoizedProps}catch(Fe){Bt(B,B.return,Fe)}}}else if(l.tag===18){if(f===null){B=l;try{var ce=B.stateNode;S?Xy(ce,!0):Xy(B.stateNode,!1)}catch(Fe){Bt(B,B.return,Fe)}}}else if((l.tag!==22&&l.tag!==23||l.memoizedState===null||l===o)&&l.child!==null){l.child.return=l,l=l.child;continue}if(l===o)break e;for(;l.sibling===null;){if(l.return===null||l.return===o)break e;f===l&&(f=null),l=l.return}f===l&&(f=null),l.sibling.return=l.return,l=l.sibling}E&4&&(E=o.updateQueue,E!==null&&(f=E.retryQueue,f!==null&&(E.retryQueue=null,Hh(o,f))));break;case 19:Ar(l,o),gr(o),E&4&&(E=o.updateQueue,E!==null&&(o.updateQueue=null,Hh(o,E)));break;case 30:break;case 21:break;default:Ar(l,o),gr(o)}}function gr(o){var l=o.flags;if(l&2){try{for(var f,E=o.return;E!==null;){if(qL(E)){f=E;break}E=E.return}if(f==null)throw Error(r(160));switch(f.tag){case 27:var S=f.stateNode,A=ZS(o);Gh(o,A,S);break;case 5:var _=f.stateNode;f.flags&32&&(kl(_,""),f.flags&=-33);var P=ZS(o);Gh(o,P,_);break;case 3:case 4:var B=f.stateNode.containerInfo,J=ZS(o);em(o,J,B);break;default:throw Error(r(161))}}catch(fe){Bt(o,o.return,fe)}o.flags&=-3}l&4096&&(o.flags&=-4097)}function sy(o){if(o.subtreeFlags&1024)for(o=o.child;o!==null;){var l=o;sy(l),l.tag===5&&l.flags&1024&&l.stateNode.reset(),o=o.sibling}}function qs(o,l){if(l.subtreeFlags&8772)for(l=l.child;l!==null;)ZL(o,l.alternate,l),l=l.sibling}function Ka(o){for(o=o.child;o!==null;){var l=o;switch(l.tag){case 0:case 11:case 14:case 15:Go(4,l,l.return),Ka(l);break;case 1:ns(l,l.return);var f=l.stateNode;typeof f.componentWillUnmount=="function"&&XL(l,l.return,f),Ka(l);break;case 27:hf(l.stateNode);case 26:case 5:ns(l,l.return),Ka(l);break;case 22:l.memoizedState===null&&Ka(l);break;case 30:Ka(l);break;default:Ka(l)}o=o.sibling}}function Js(o,l,f){for(f=f&&(l.subtreeFlags&8772)!==0,l=l.child;l!==null;){var E=l.alternate,S=o,A=l,_=A.flags;switch(A.tag){case 0:case 11:case 15:Js(S,A,f),nf(4,A);break;case 1:if(Js(S,A,f),E=A,S=E.stateNode,typeof S.componentDidMount=="function")try{S.componentDidMount()}catch(J){Bt(E,E.return,J)}if(E=A,S=E.updateQueue,S!==null){var P=E.stateNode;try{var B=S.shared.hiddenCallbacks;if(B!==null)for(S.shared.hiddenCallbacks=null,S=0;SVt&&(_=Vt,Vt=et,et=_);var W=l_(P,et),k=l_(P,Vt);if(W&&k&&(ce.rangeCount!==1||ce.anchorNode!==W.node||ce.anchorOffset!==W.offset||ce.focusNode!==k.node||ce.focusOffset!==k.offset)){var K=Te.createRange();K.setStart(W.node,W.offset),ce.removeAllRanges(),et>Vt?(ce.addRange(K),ce.extend(k.node,k.offset)):(K.setEnd(k.node,k.offset),ce.addRange(K))}}}}for(Te=[],ce=P;ce=ce.parentNode;)ce.nodeType===1&&Te.push({element:ce,left:ce.scrollLeft,top:ce.scrollTop});for(typeof P.focus=="function"&&P.focus(),P=0;Pf?32:f,U.T=null,f=lm,lm=null;var A=Vo,_=Zs;if(Dn=0,fc=Vo=null,Zs=0,(xt&6)!==0)throw Error(r(331));var P=xt;if(xt|=4,cy(A.current),oy(A,A.current,_,f),xt=P,uf(0,!1),An&&typeof An.onPostCommitFiberRoot=="function")try{An.onPostCommitFiberRoot(ti,A)}catch{}return!0}finally{Z.p=S,U.T=E,Ly(o,l)}}function vy(o,l,f){l=ai(f,l),l=kS(o.stateNode,l,2),o=wo(o,l,2),o!==null&&(G(o,2),rs(o))}function Bt(o,l,f){if(o.tag===3)vy(o,o,f);else for(;l!==null;){if(l.tag===3){vy(l,o,f);break}else if(l.tag===1){var E=l.stateNode;if(typeof l.type.getDerivedStateFromError=="function"||typeof E.componentDidCatch=="function"&&(ko===null||!ko.has(E))){o=ai(f,o),f=DL(2),E=wo(l,f,2),E!==null&&(bL(f,E,l,o),G(E,2),rs(E));break}}l=l.return}}function dm(o,l,f){var E=o.pingCache;if(E===null){E=o.pingCache=new lY;var S=new Set;E.set(l,S)}else S=E.get(l),S===void 0&&(S=new Set,E.set(l,S));S.has(f)||(im=!0,S.add(f),o=hY.bind(null,o,l,f),l.then(o,o))}function hY(o,l,f){var E=o.pingCache;E!==null&&E.delete(l),o.pingedLanes|=o.suspendedLanes&f,o.warmLanes&=~f,$t===o&&(gt&f)===f&&(fn===4||fn===3&&(gt&62914560)===gt&&300>Oe()-kh?(xt&2)===0&&dc(o,0):sm|=f,uc===gt&&(uc=0)),rs(o)}function Dy(o,l){l===0&&(l=th()),o=Ga(o,l),o!==null&&(G(o,l),rs(o))}function EY(o){var l=o.memoizedState,f=0;l!==null&&(f=l.retryLane),Dy(o,f)}function TY(o,l){var f=0;switch(o.tag){case 31:case 13:var E=o.stateNode,S=o.memoizedState;S!==null&&(f=S.retryLane);break;case 19:E=o.stateNode;break;case 22:E=o.stateNode._retryCache;break;default:throw Error(r(314))}E!==null&&E.delete(l),Dy(o,f)}function pY(o,l){return H(o,l)}var Xh=null,Ec=null,hm=!1,Kh=!1,Em=!1,Wo=0;function rs(o){o!==Ec&&o.next===null&&(Ec===null?Xh=Ec=o:Ec=Ec.next=o),Kh=!0,hm||(hm=!0,SY())}function uf(o,l){if(!Em&&Kh){Em=!0;do for(var f=!1,E=Xh;E!==null;){if(o!==0){var S=E.pendingLanes;if(S===0)var A=0;else{var _=E.suspendedLanes,P=E.pingedLanes;A=(1<<31-Bn(42|o)+1)-1,A&=S&~(_&~P),A=A&201326741?A&201326741|1:A?A|2:0}A!==0&&(f=!0,xy(E,A))}else A=gt,A=Ma(E,E===$t?A:0,E.cancelPendingCommit!==null||E.timeoutHandle!==-1),(A&3)===0||qi(E,A)||(f=!0,xy(E,A));E=E.next}while(f);Em=!1}}function RY(){by()}function by(){Kh=hm=!1;var o=0;Wo!==0&&yY()&&(o=Wo);for(var l=Oe(),f=null,E=Xh;E!==null;){var S=E.next,A=My(E,l);A===0?(E.next=null,f===null?Xh=S:f.next=S,S===null&&(Ec=f)):(f=E,(o!==0||(A&3)!==0)&&(Kh=!0)),E=S}Dn!==0&&Dn!==5||uf(o),Wo!==0&&(Wo=0)}function My(o,l){for(var f=o.suspendedLanes,E=o.pingedLanes,S=o.expirationTimes,A=o.pendingLanes&-62914561;0P)break;var fe=B.transferSize,Te=B.initiatorType;fe&&Vy(Te)&&(B=B.responseEnd,_+=fe*(B"u"?null:document;function ev(o,l,f){var E=Tc;if(E&&typeof l=="string"&&l){var S=si(l);S='link[rel="'+o+'"][href="'+S+'"]',typeof f=="string"&&(S+='[crossorigin="'+f+'"]'),Zy.has(S)||(Zy.add(S),o={rel:o,crossOrigin:f,href:l},E.querySelector(S)===null&&(l=E.createElement("link"),Kn(l,"link",o),rn(l),E.head.appendChild(l)))}}function BY(o){eo.D(o),ev("dns-prefetch",o,null)}function GY(o,l){eo.C(o,l),ev("preconnect",o,l)}function HY(o,l,f){eo.L(o,l,f);var E=Tc;if(E&&o&&l){var S='link[rel="preload"][as="'+si(l)+'"]';l==="image"&&f&&f.imageSrcSet?(S+='[imagesrcset="'+si(f.imageSrcSet)+'"]',typeof f.imageSizes=="string"&&(S+='[imagesizes="'+si(f.imageSizes)+'"]')):S+='[href="'+si(o)+'"]';var A=S;switch(l){case"style":A=pc(o);break;case"script":A=Rc(o)}hi.has(A)||(o=T({rel:"preload",href:l==="image"&&f&&f.imageSrcSet?void 0:o,as:l},f),hi.set(A,o),E.querySelector(S)!==null||l==="style"&&E.querySelector(Ef(A))||l==="script"&&E.querySelector(Tf(A))||(l=E.createElement("link"),Kn(l,"link",o),rn(l),E.head.appendChild(l)))}}function FY(o,l){eo.m(o,l);var f=Tc;if(f&&o){var E=l&&typeof l.as=="string"?l.as:"script",S='link[rel="modulepreload"][as="'+si(E)+'"][href="'+si(o)+'"]',A=S;switch(E){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":A=Rc(o)}if(!hi.has(A)&&(o=T({rel:"modulepreload",href:o},l),hi.set(A,o),f.querySelector(S)===null)){switch(E){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(f.querySelector(Tf(A)))return}E=f.createElement("link"),Kn(E,"link",o),rn(E),f.head.appendChild(E)}}}function kY(o,l,f){eo.S(o,l,f);var E=Tc;if(E&&o){var S=Ji(E).hoistableStyles,A=pc(o);l=l||"default";var _=S.get(A);if(!_){var P={loading:0,preload:null};if(_=E.querySelector(Ef(A)))P.loading=5;else{o=T({rel:"stylesheet",href:o,"data-precedence":l},f),(f=hi.get(A))&&Dm(o,f);var B=_=E.createElement("link");rn(B),Kn(B,"link",o),B._p=new Promise(function(J,fe){B.onload=J,B.onerror=fe}),B.addEventListener("load",function(){P.loading|=1}),B.addEventListener("error",function(){P.loading|=2}),P.loading|=4,eE(_,l,E)}_={type:"stylesheet",instance:_,count:1,state:P},S.set(A,_)}}}function VY(o,l){eo.X(o,l);var f=Tc;if(f&&o){var E=Ji(f).hoistableScripts,S=Rc(o),A=E.get(S);A||(A=f.querySelector(Tf(S)),A||(o=T({src:o,async:!0},l),(l=hi.get(S))&&bm(o,l),A=f.createElement("script"),rn(A),Kn(A,"link",o),f.head.appendChild(A)),A={type:"script",instance:A,count:1,state:null},E.set(S,A))}}function YY(o,l){eo.M(o,l);var f=Tc;if(f&&o){var E=Ji(f).hoistableScripts,S=Rc(o),A=E.get(S);A||(A=f.querySelector(Tf(S)),A||(o=T({src:o,async:!0,type:"module"},l),(l=hi.get(S))&&bm(o,l),A=f.createElement("script"),rn(A),Kn(A,"link",o),f.head.appendChild(A)),A={type:"script",instance:A,count:1,state:null},E.set(S,A))}}function tv(o,l,f,E){var S=(S=me.current)?Zh(S):null;if(!S)throw Error(r(446));switch(o){case"meta":case"title":return null;case"style":return typeof f.precedence=="string"&&typeof f.href=="string"?(l=pc(f.href),f=Ji(S).hoistableStyles,E=f.get(l),E||(E={type:"style",instance:null,count:0,state:null},f.set(l,E)),E):{type:"void",instance:null,count:0,state:null};case"link":if(f.rel==="stylesheet"&&typeof f.href=="string"&&typeof f.precedence=="string"){o=pc(f.href);var A=Ji(S).hoistableStyles,_=A.get(o);if(_||(S=S.ownerDocument||S,_={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},A.set(o,_),(A=S.querySelector(Ef(o)))&&!A._p&&(_.instance=A,_.state.loading=5),hi.has(o)||(f={rel:"preload",as:"style",href:f.href,crossOrigin:f.crossOrigin,integrity:f.integrity,media:f.media,hrefLang:f.hrefLang,referrerPolicy:f.referrerPolicy},hi.set(o,f),A||WY(S,o,f,_.state))),l&&E===null)throw Error(r(528,""));return _}if(l&&E!==null)throw Error(r(529,""));return null;case"script":return l=f.async,f=f.src,typeof f=="string"&&l&&typeof l!="function"&&typeof l!="symbol"?(l=Rc(f),f=Ji(S).hoistableScripts,E=f.get(l),E||(E={type:"script",instance:null,count:0,state:null},f.set(l,E)),E):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,o))}}function pc(o){return'href="'+si(o)+'"'}function Ef(o){return'link[rel="stylesheet"]['+o+"]"}function nv(o){return T({},o,{"data-precedence":o.precedence,precedence:null})}function WY(o,l,f,E){o.querySelector('link[rel="preload"][as="style"]['+l+"]")?E.loading=1:(l=o.createElement("link"),E.preload=l,l.addEventListener("load",function(){return E.loading|=1}),l.addEventListener("error",function(){return E.loading|=2}),Kn(l,"link",f),rn(l),o.head.appendChild(l))}function Rc(o){return'[src="'+si(o)+'"]'}function Tf(o){return"script[async]"+o}function rv(o,l,f){if(l.count++,l.instance===null)switch(l.type){case"style":var E=o.querySelector('style[data-href~="'+si(f.href)+'"]');if(E)return l.instance=E,rn(E),E;var S=T({},f,{"data-href":f.href,"data-precedence":f.precedence,href:null,precedence:null});return E=(o.ownerDocument||o).createElement("style"),rn(E),Kn(E,"style",S),eE(E,f.precedence,o),l.instance=E;case"stylesheet":S=pc(f.href);var A=o.querySelector(Ef(S));if(A)return l.state.loading|=4,l.instance=A,rn(A),A;E=nv(f),(S=hi.get(S))&&Dm(E,S),A=(o.ownerDocument||o).createElement("link"),rn(A);var _=A;return _._p=new Promise(function(P,B){_.onload=P,_.onerror=B}),Kn(A,"link",E),l.state.loading|=4,eE(A,f.precedence,o),l.instance=A;case"script":return A=Rc(f.src),(S=o.querySelector(Tf(A)))?(l.instance=S,rn(S),S):(E=f,(S=hi.get(A))&&(E=T({},f),bm(E,S)),o=o.ownerDocument||o,S=o.createElement("script"),rn(S),Kn(S,"link",E),o.head.appendChild(S),l.instance=S);case"void":return null;default:throw Error(r(443,l.type))}else l.type==="stylesheet"&&(l.state.loading&4)===0&&(E=l.instance,l.state.loading|=4,eE(E,f.precedence,o));return l.instance}function eE(o,l,f){for(var E=f.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),S=E.length?E[E.length-1]:null,A=S,_=0;_ title"):null)}function $Y(o,l,f){if(f===1||l.itemProp!=null)return!1;switch(o){case"meta":case"title":return!0;case"style":if(typeof l.precedence!="string"||typeof l.href!="string"||l.href==="")break;return!0;case"link":if(typeof l.rel!="string"||typeof l.href!="string"||l.href===""||l.onLoad||l.onError)break;switch(l.rel){case"stylesheet":return o=l.disabled,typeof l.precedence=="string"&&o==null;default:return!0}case"script":if(l.async&&typeof l.async!="function"&&typeof l.async!="symbol"&&!l.onLoad&&!l.onError&&l.src&&typeof l.src=="string")return!0}return!1}function ov(o){return!(o.type==="stylesheet"&&(o.state.loading&3)===0)}function zY(o,l,f,E){if(f.type==="stylesheet"&&(typeof E.media!="string"||matchMedia(E.media).matches!==!1)&&(f.state.loading&4)===0){if(f.instance===null){var S=pc(E.href),A=l.querySelector(Ef(S));if(A){l=A._p,l!==null&&typeof l=="object"&&typeof l.then=="function"&&(o.count++,o=nE.bind(o),l.then(o,o)),f.state.loading|=4,f.instance=A,rn(A);return}A=l.ownerDocument||l,E=nv(E),(S=hi.get(S))&&Dm(E,S),A=A.createElement("link"),rn(A);var _=A;_._p=new Promise(function(P,B){_.onload=P,_.onerror=B}),Kn(A,"link",E),f.instance=A}o.stylesheets===null&&(o.stylesheets=new Map),o.stylesheets.set(f,l),(l=f.state.preload)&&(f.state.loading&3)===0&&(o.count++,f=nE.bind(o),l.addEventListener("load",f),l.addEventListener("error",f))}}var Mm=0;function jY(o,l){return o.stylesheets&&o.count===0&&iE(o,o.stylesheets),0Mm?50:800)+l);return o.unsuspend=f,function(){o.unsuspend=null,clearTimeout(E),clearTimeout(S)}}:null}function nE(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)iE(this,this.stylesheets);else if(this.unsuspend){var o=this.unsuspend;this.unsuspend=null,o()}}}var rE=null;function iE(o,l){o.stylesheets=null,o.unsuspend!==null&&(o.count++,rE=new Map,l.forEach(XY,o),rE=null,nE.call(o))}function XY(o,l){if(!(l.state.loading&4)){var f=rE.get(o);if(f)var E=f.get(null);else{f=new Map,rE.set(o,f);for(var S=o.querySelectorAll("link[data-precedence],style[data-precedence]"),A=0;A"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),km.exports=u2(),km.exports}var d2=f2(),mu=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(t){return this.listeners.add(t),this.onSubscribe(),()=>{this.listeners.delete(t),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},h2={setTimeout:(t,e)=>setTimeout(t,e),clearTimeout:t=>clearTimeout(t),setInterval:(t,e)=>setInterval(t,e),clearInterval:t=>clearInterval(t)},E2=class{#e=h2;#t=!1;setTimeoutProvider(t){this.#e=t}setTimeout(t,e){return this.#e.setTimeout(t,e)}clearTimeout(t){this.#e.clearTimeout(t)}setInterval(t,e){return this.#e.setInterval(t,e)}clearInterval(t){this.#e.clearInterval(t)}},al=new E2;function T2(t){setTimeout(t,0)}var pl=typeof window>"u"||"Deno"in globalThis;function dr(){}function p2(t,e){return typeof t=="function"?t(e):t}function Mg(t){return typeof t=="number"&&t>=0&&t!==1/0}function RP(t,e){return Math.max(t+(e||0)-Date.now(),0)}function da(t,e){return typeof t=="function"?t(e):t}function Ti(t,e){return typeof t=="function"?t(e):t}function Dv(t,e){const{type:n="all",exact:r,fetchStatus:i,predicate:s,queryKey:a,stale:c}=t;if(a){if(r){if(e.queryHash!==LN(a,e.options))return!1}else if(!Zf(e.queryKey,a))return!1}if(n!=="all"){const u=e.isActive();if(n==="active"&&!u||n==="inactive"&&u)return!1}return!(typeof c=="boolean"&&e.isStale()!==c||i&&i!==e.state.fetchStatus||s&&!s(e))}function bv(t,e){const{exact:n,status:r,predicate:i,mutationKey:s}=t;if(s){if(!e.options.mutationKey)return!1;if(n){if(Rl(e.options.mutationKey)!==Rl(s))return!1}else if(!Zf(e.options.mutationKey,s))return!1}return!(r&&e.state.status!==r||i&&!i(e))}function LN(t,e){return(e?.queryKeyHashFn||Rl)(t)}function Rl(t){return JSON.stringify(t,(e,n)=>Pg(n)?Object.keys(n).sort().reduce((r,i)=>(r[i]=n[i],r),{}):n)}function Zf(t,e){return t===e?!0:typeof t!=typeof e?!1:t&&e&&typeof t=="object"&&typeof e=="object"?Object.keys(e).every(n=>Zf(t[n],e[n])):!1}var R2=Object.prototype.hasOwnProperty;function SP(t,e){if(t===e)return t;const n=Mv(t)&&Mv(e);if(!n&&!(Pg(t)&&Pg(e)))return e;const i=(n?t:Object.keys(t)).length,s=n?e:Object.keys(e),a=s.length,c=n?new Array(a):{};let u=0;for(let d=0;d{al.setTimeout(e,t)})}function xg(t,e,n){return typeof n.structuralSharing=="function"?n.structuralSharing(t,e):n.structuralSharing!==!1?SP(t,e):e}function m2(t,e,n=0){const r=[...t,e];return n&&r.length>n?r.slice(1):r}function A2(t,e,n=0){const r=[e,...t];return n&&r.length>n?r.slice(0,-1):r}var yN=Symbol();function mP(t,e){return!t.queryFn&&e?.initialPromise?()=>e.initialPromise:!t.queryFn||t.queryFn===yN?()=>Promise.reject(new Error(`Missing queryFn: '${t.queryHash}'`)):t.queryFn}function AP(t,e){return typeof t=="function"?t(...e):!!t}var g2=class extends mu{#e;#t;#n;constructor(){super(),this.#n=t=>{if(!pl&&window.addEventListener){const e=()=>t();return window.addEventListener("visibilitychange",e,!1),()=>{window.removeEventListener("visibilitychange",e)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(t){this.#n=t,this.#t?.(),this.#t=t(e=>{typeof e=="boolean"?this.setFocused(e):this.onFocus()})}setFocused(t){this.#e!==t&&(this.#e=t,this.onFocus())}onFocus(){const t=this.isFocused();this.listeners.forEach(e=>{e(t)})}isFocused(){return typeof this.#e=="boolean"?this.#e:globalThis.document?.visibilityState!=="hidden"}},vN=new g2;function wg(){let t,e;const n=new Promise((i,s)=>{t=i,e=s});n.status="pending",n.catch(()=>{});function r(i){Object.assign(n,i),delete n.resolve,delete n.reject}return n.resolve=i=>{r({status:"fulfilled",value:i}),t(i)},n.reject=i=>{r({status:"rejected",reason:i}),e(i)},n}var O2=T2;function N2(){let t=[],e=0,n=c=>{c()},r=c=>{c()},i=O2;const s=c=>{e?t.push(c):i(()=>{n(c)})},a=()=>{const c=t;t=[],c.length&&i(()=>{r(()=>{c.forEach(u=>{n(u)})})})};return{batch:c=>{let u;e++;try{u=c()}finally{e--,e||a()}return u},batchCalls:c=>(...u)=>{s(()=>{c(...u)})},schedule:s,setNotifyFunction:c=>{n=c},setBatchNotifyFunction:c=>{r=c},setScheduler:c=>{i=c}}}var xn=N2(),I2=class extends mu{#e=!0;#t;#n;constructor(){super(),this.#n=t=>{if(!pl&&window.addEventListener){const e=()=>t(!0),n=()=>t(!1);return window.addEventListener("online",e,!1),window.addEventListener("offline",n,!1),()=>{window.removeEventListener("online",e),window.removeEventListener("offline",n)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(t){this.#n=t,this.#t?.(),this.#t=t(this.setOnline.bind(this))}setOnline(t){this.#e!==t&&(this.#e=t,this.listeners.forEach(n=>{n(t)}))}isOnline(){return this.#e}},yT=new I2;function C2(t){return Math.min(1e3*2**t,3e4)}function gP(t){return(t??"online")==="online"?yT.isOnline():!0}var Ug=class extends Error{constructor(t){super("CancelledError"),this.revert=t?.revert,this.silent=t?.silent}};function OP(t){let e=!1,n=0,r;const i=wg(),s=()=>i.status!=="pending",a=N=>{if(!s()){const I=new Ug(N);p(I),t.onCancel?.(I)}},c=()=>{e=!0},u=()=>{e=!1},d=()=>vN.isFocused()&&(t.networkMode==="always"||yT.isOnline())&&t.canRun(),h=()=>gP(t.networkMode)&&t.canRun(),T=N=>{s()||(r?.(),i.resolve(N))},p=N=>{s()||(r?.(),i.reject(N))},R=()=>new Promise(N=>{r=I=>{(s()||d())&&N(I)},t.onPause?.()}).then(()=>{r=void 0,s()||t.onContinue?.()}),g=()=>{if(s())return;let N;const I=n===0?t.initialPromise:void 0;try{N=I??t.fn()}catch(C){N=Promise.reject(C)}Promise.resolve(N).then(T).catch(C=>{if(s())return;const L=t.retry??(pl?0:3),y=t.retryDelay??C2,v=typeof y=="function"?y(n,C):y,b=L===!0||typeof L=="number"&&nd()?void 0:R()).then(()=>{e?p(C):g()})})};return{promise:i,status:()=>i.status,cancel:a,continue:()=>(r?.(),i),cancelRetry:c,continueRetry:u,canStart:h,start:()=>(h()?g():R().then(g),i)}}var NP=class{#e;destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),Mg(this.gcTime)&&(this.#e=al.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(t){this.gcTime=Math.max(this.gcTime||0,t??(pl?1/0:300*1e3))}clearGcTimeout(){this.#e&&(al.clearTimeout(this.#e),this.#e=void 0)}},_2=class extends NP{#e;#t;#n;#i;#r;#o;#a;constructor(e){super(),this.#a=!1,this.#o=e.defaultOptions,this.setOptions(e.options),this.observers=[],this.#i=e.client,this.#n=this.#i.getQueryCache(),this.queryKey=e.queryKey,this.queryHash=e.queryHash,this.#e=xv(this.options),this.state=e.state??this.#e,this.scheduleGc()}get meta(){return this.options.meta}get promise(){return this.#r?.promise}setOptions(e){if(this.options={...this.#o,...e},this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const n=xv(this.options);n.data!==void 0&&(this.setData(n.data,{updatedAt:n.dataUpdatedAt,manual:!0}),this.#e=n)}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&this.#n.remove(this)}setData(e,n){const r=xg(this.state.data,e,this.options);return this.#s({data:r,type:"success",dataUpdatedAt:n?.updatedAt,manual:n?.manual}),r}setState(e,n){this.#s({type:"setState",state:e,setStateOptions:n})}cancel(e){const n=this.#r?.promise;return this.#r?.cancel(e),n?n.then(dr).catch(dr):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}reset(){this.destroy(),this.setState(this.#e)}isActive(){return this.observers.some(e=>Ti(e.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===yN||this.state.dataUpdateCount+this.state.errorUpdateCount===0}isStatic(){return this.getObserversCount()>0?this.observers.some(e=>da(e.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(e=>e.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(e=0){return this.state.data===void 0?!0:e==="static"?!1:this.state.isInvalidated?!0:!RP(this.state.dataUpdatedAt,e)}onFocus(){this.observers.find(n=>n.shouldFetchOnWindowFocus())?.refetch({cancelRefetch:!1}),this.#r?.continue()}onOnline(){this.observers.find(n=>n.shouldFetchOnReconnect())?.refetch({cancelRefetch:!1}),this.#r?.continue()}addObserver(e){this.observers.includes(e)||(this.observers.push(e),this.clearGcTimeout(),this.#n.notify({type:"observerAdded",query:this,observer:e}))}removeObserver(e){this.observers.includes(e)&&(this.observers=this.observers.filter(n=>n!==e),this.observers.length||(this.#r&&(this.#a?this.#r.cancel({revert:!0}):this.#r.cancelRetry()),this.scheduleGc()),this.#n.notify({type:"observerRemoved",query:this,observer:e}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||this.#s({type:"invalidate"})}async fetch(e,n){if(this.state.fetchStatus!=="idle"&&this.#r?.status()!=="rejected"){if(this.state.data!==void 0&&n?.cancelRefetch)this.cancel({silent:!0});else if(this.#r)return this.#r.continueRetry(),this.#r.promise}if(e&&this.setOptions(e),!this.options.queryFn){const u=this.observers.find(d=>d.options.queryFn);u&&this.setOptions(u.options)}const r=new AbortController,i=u=>{Object.defineProperty(u,"signal",{enumerable:!0,get:()=>(this.#a=!0,r.signal)})},s=()=>{const u=mP(this.options,n),h=(()=>{const T={client:this.#i,queryKey:this.queryKey,meta:this.meta};return i(T),T})();return this.#a=!1,this.options.persister?this.options.persister(u,h,this):u(h)},c=(()=>{const u={fetchOptions:n,options:this.options,queryKey:this.queryKey,client:this.#i,state:this.state,fetchFn:s};return i(u),u})();this.options.behavior?.onFetch(c,this),this.#t=this.state,(this.state.fetchStatus==="idle"||this.state.fetchMeta!==c.fetchOptions?.meta)&&this.#s({type:"fetch",meta:c.fetchOptions?.meta}),this.#r=OP({initialPromise:n?.initialPromise,fn:c.fetchFn,onCancel:u=>{u instanceof Ug&&u.revert&&this.setState({...this.#t,fetchStatus:"idle"}),r.abort()},onFail:(u,d)=>{this.#s({type:"failed",failureCount:u,error:d})},onPause:()=>{this.#s({type:"pause"})},onContinue:()=>{this.#s({type:"continue"})},retry:c.options.retry,retryDelay:c.options.retryDelay,networkMode:c.options.networkMode,canRun:()=>!0});try{const u=await this.#r.start();if(u===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(u),this.#n.config.onSuccess?.(u,this),this.#n.config.onSettled?.(u,this.state.error,this),u}catch(u){if(u instanceof Ug){if(u.silent)return this.#r.promise;if(u.revert){if(this.state.data===void 0)throw u;return this.state.data}}throw this.#s({type:"error",error:u}),this.#n.config.onError?.(u,this),this.#n.config.onSettled?.(this.state.data,u,this),u}finally{this.scheduleGc()}}#s(e){const n=r=>{switch(e.type){case"failed":return{...r,fetchFailureCount:e.failureCount,fetchFailureReason:e.error};case"pause":return{...r,fetchStatus:"paused"};case"continue":return{...r,fetchStatus:"fetching"};case"fetch":return{...r,...IP(r.data,this.options),fetchMeta:e.meta??null};case"success":const i={...r,data:e.data,dataUpdateCount:r.dataUpdateCount+1,dataUpdatedAt:e.dataUpdatedAt??Date.now(),error:null,isInvalidated:!1,status:"success",...!e.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return this.#t=e.manual?i:void 0,i;case"error":const s=e.error;return{...r,error:s,errorUpdateCount:r.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:r.fetchFailureCount+1,fetchFailureReason:s,fetchStatus:"idle",status:"error"};case"invalidate":return{...r,isInvalidated:!0};case"setState":return{...r,...e.state}}};this.state=n(this.state),xn.batch(()=>{this.observers.forEach(r=>{r.onQueryUpdate()}),this.#n.notify({query:this,type:"updated",action:e})})}};function IP(t,e){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:gP(e.networkMode)?"fetching":"paused",...t===void 0&&{error:null,status:"pending"}}}function xv(t){const e=typeof t.initialData=="function"?t.initialData():t.initialData,n=e!==void 0,r=n?typeof t.initialDataUpdatedAt=="function"?t.initialDataUpdatedAt():t.initialDataUpdatedAt:0;return{data:e,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?"success":"pending",fetchStatus:"idle"}}var L2=class extends mu{constructor(t,e){super(),this.options=e,this.#e=t,this.#s=null,this.#a=wg(),this.bindMethods(),this.setOptions(e)}#e;#t=void 0;#n=void 0;#i=void 0;#r;#o;#a;#s;#T;#d;#h;#c;#u;#l;#E=new Set;bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(this.#t.addObserver(this),wv(this.#t,this.options)?this.#f():this.updateResult(),this.#m())}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Bg(this.#t,this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Bg(this.#t,this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,this.#A(),this.#g(),this.#t.removeObserver(this)}setOptions(t){const e=this.options,n=this.#t;if(this.options=this.#e.defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof Ti(this.options.enabled,this.#t)!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");this.#O(),this.#t.setOptions(this.options),e._defaulted&&!LT(this.options,e)&&this.#e.getQueryCache().notify({type:"observerOptionsUpdated",query:this.#t,observer:this});const r=this.hasListeners();r&&Uv(this.#t,n,this.options,e)&&this.#f(),this.updateResult(),r&&(this.#t!==n||Ti(this.options.enabled,this.#t)!==Ti(e.enabled,this.#t)||da(this.options.staleTime,this.#t)!==da(e.staleTime,this.#t))&&this.#p();const i=this.#R();r&&(this.#t!==n||Ti(this.options.enabled,this.#t)!==Ti(e.enabled,this.#t)||i!==this.#l)&&this.#S(i)}getOptimisticResult(t){const e=this.#e.getQueryCache().build(this.#e,t),n=this.createResult(e,t);return v2(this,n)&&(this.#i=n,this.#o=this.options,this.#r=this.#t.state),n}getCurrentResult(){return this.#i}trackResult(t,e){return new Proxy(t,{get:(n,r)=>(this.trackProp(r),e?.(r),r==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&this.#a.status==="pending"&&this.#a.reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(n,r))})}trackProp(t){this.#E.add(t)}getCurrentQuery(){return this.#t}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const e=this.#e.defaultQueryOptions(t),n=this.#e.getQueryCache().build(this.#e,e);return n.fetch().then(()=>this.createResult(n,e))}fetch(t){return this.#f({...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),this.#i))}#f(t){this.#O();let e=this.#t.fetch(this.options,t);return t?.throwOnError||(e=e.catch(dr)),e}#p(){this.#A();const t=da(this.options.staleTime,this.#t);if(pl||this.#i.isStale||!Mg(t))return;const n=RP(this.#i.dataUpdatedAt,t)+1;this.#c=al.setTimeout(()=>{this.#i.isStale||this.updateResult()},n)}#R(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(this.#t):this.options.refetchInterval)??!1}#S(t){this.#g(),this.#l=t,!(pl||Ti(this.options.enabled,this.#t)===!1||!Mg(this.#l)||this.#l===0)&&(this.#u=al.setInterval(()=>{(this.options.refetchIntervalInBackground||vN.isFocused())&&this.#f()},this.#l))}#m(){this.#p(),this.#S(this.#R())}#A(){this.#c&&(al.clearTimeout(this.#c),this.#c=void 0)}#g(){this.#u&&(al.clearInterval(this.#u),this.#u=void 0)}createResult(t,e){const n=this.#t,r=this.options,i=this.#i,s=this.#r,a=this.#o,u=t!==n?t.state:this.#n,{state:d}=t;let h={...d},T=!1,p;if(e._optimisticResults){const w=this.hasListeners(),F=!w&&wv(t,e),Q=w&&Uv(t,n,e,r);(F||Q)&&(h={...h,...IP(d.data,t.options)}),e._optimisticResults==="isRestoring"&&(h.fetchStatus="idle")}let{error:R,errorUpdatedAt:g,status:N}=h;p=h.data;let I=!1;if(e.placeholderData!==void 0&&p===void 0&&N==="pending"){let w;i?.isPlaceholderData&&e.placeholderData===a?.placeholderData?(w=i.data,I=!0):w=typeof e.placeholderData=="function"?e.placeholderData(this.#h?.state.data,this.#h):e.placeholderData,w!==void 0&&(N="success",p=xg(i?.data,w,e),T=!0)}if(e.select&&p!==void 0&&!I)if(i&&p===s?.data&&e.select===this.#T)p=this.#d;else try{this.#T=e.select,p=e.select(p),p=xg(i?.data,p,e),this.#d=p,this.#s=null}catch(w){this.#s=w}this.#s&&(R=this.#s,p=this.#d,g=Date.now(),N="error");const C=h.fetchStatus==="fetching",L=N==="pending",y=N==="error",v=L&&C,b=p!==void 0,M={status:N,fetchStatus:h.fetchStatus,isPending:L,isSuccess:N==="success",isError:y,isInitialLoading:v,isLoading:v,data:p,dataUpdatedAt:h.dataUpdatedAt,error:R,errorUpdatedAt:g,failureCount:h.fetchFailureCount,failureReason:h.fetchFailureReason,errorUpdateCount:h.errorUpdateCount,isFetched:h.dataUpdateCount>0||h.errorUpdateCount>0,isFetchedAfterMount:h.dataUpdateCount>u.dataUpdateCount||h.errorUpdateCount>u.errorUpdateCount,isFetching:C,isRefetching:C&&!L,isLoadingError:y&&!b,isPaused:h.fetchStatus==="paused",isPlaceholderData:T,isRefetchError:y&&b,isStale:DN(t,e),refetch:this.refetch,promise:this.#a,isEnabled:Ti(e.enabled,t)!==!1};if(this.options.experimental_prefetchInRender){const w=ne=>{M.status==="error"?ne.reject(M.error):M.data!==void 0&&ne.resolve(M.data)},F=()=>{const ne=this.#a=M.promise=wg();w(ne)},Q=this.#a;switch(Q.status){case"pending":t.queryHash===n.queryHash&&w(Q);break;case"fulfilled":(M.status==="error"||M.data!==Q.value)&&F();break;case"rejected":(M.status!=="error"||M.error!==Q.reason)&&F();break}}return M}updateResult(){const t=this.#i,e=this.createResult(this.#t,this.options);if(this.#r=this.#t.state,this.#o=this.options,this.#r.data!==void 0&&(this.#h=this.#t),LT(e,t))return;this.#i=e;const n=()=>{if(!t)return!0;const{notifyOnChangeProps:r}=this.options,i=typeof r=="function"?r():r;if(i==="all"||!i&&!this.#E.size)return!0;const s=new Set(i??this.#E);return this.options.throwOnError&&s.add("error"),Object.keys(this.#i).some(a=>{const c=a;return this.#i[c]!==t[c]&&s.has(c)})};this.#N({listeners:n()})}#O(){const t=this.#e.getQueryCache().build(this.#e,this.options);if(t===this.#t)return;const e=this.#t;this.#t=t,this.#n=t.state,this.hasListeners()&&(e?.removeObserver(this),t.addObserver(this))}onQueryUpdate(){this.updateResult(),this.hasListeners()&&this.#m()}#N(t){xn.batch(()=>{t.listeners&&this.listeners.forEach(e=>{e(this.#i)}),this.#e.getQueryCache().notify({query:this.#t,type:"observerResultsUpdated"})})}};function y2(t,e){return Ti(e.enabled,t)!==!1&&t.state.data===void 0&&!(t.state.status==="error"&&e.retryOnMount===!1)}function wv(t,e){return y2(t,e)||t.state.data!==void 0&&Bg(t,e,e.refetchOnMount)}function Bg(t,e,n){if(Ti(e.enabled,t)!==!1&&da(e.staleTime,t)!=="static"){const r=typeof n=="function"?n(t):n;return r==="always"||r!==!1&&DN(t,e)}return!1}function Uv(t,e,n,r){return(t!==e||Ti(r.enabled,t)===!1)&&(!n.suspense||t.state.status!=="error")&&DN(t,n)}function DN(t,e){return Ti(e.enabled,t)!==!1&&t.isStaleByTime(da(e.staleTime,t))}function v2(t,e){return!LT(t.getCurrentResult(),e)}function Bv(t){return{onFetch:(e,n)=>{const r=e.options,i=e.fetchOptions?.meta?.fetchMore?.direction,s=e.state.data?.pages||[],a=e.state.data?.pageParams||[];let c={pages:[],pageParams:[]},u=0;const d=async()=>{let h=!1;const T=g=>{Object.defineProperty(g,"signal",{enumerable:!0,get:()=>(e.signal.aborted?h=!0:e.signal.addEventListener("abort",()=>{h=!0}),e.signal)})},p=mP(e.options,e.fetchOptions),R=async(g,N,I)=>{if(h)return Promise.reject();if(N==null&&g.pages.length)return Promise.resolve(g);const L=(()=>{const D={client:e.client,queryKey:e.queryKey,pageParam:N,direction:I?"backward":"forward",meta:e.options.meta};return T(D),D})(),y=await p(L),{maxPages:v}=e.options,b=I?A2:m2;return{pages:b(g.pages,y,v),pageParams:b(g.pageParams,N,v)}};if(i&&s.length){const g=i==="backward",N=g?D2:Gv,I={pages:s,pageParams:a},C=N(r,I);c=await R(I,C,g)}else{const g=t??s.length;do{const N=u===0?a[0]??r.initialPageParam:Gv(r,c);if(u>0&&N==null)break;c=await R(c,N),u++}while(ue.options.persister?.(d,{client:e.client,queryKey:e.queryKey,meta:e.options.meta,signal:e.signal},n):e.fetchFn=d}}}function Gv(t,{pages:e,pageParams:n}){const r=e.length-1;return e.length>0?t.getNextPageParam(e[r],e,n[r],n):void 0}function D2(t,{pages:e,pageParams:n}){return e.length>0?t.getPreviousPageParam?.(e[0],e,n[0],n):void 0}var b2=class extends NP{#e;#t;#n;#i;constructor(t){super(),this.#e=t.client,this.mutationId=t.mutationId,this.#n=t.mutationCache,this.#t=[],this.state=t.state||CP(),this.setOptions(t.options),this.scheduleGc()}setOptions(t){this.options=t,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(t){this.#t.includes(t)||(this.#t.push(t),this.clearGcTimeout(),this.#n.notify({type:"observerAdded",mutation:this,observer:t}))}removeObserver(t){this.#t=this.#t.filter(e=>e!==t),this.scheduleGc(),this.#n.notify({type:"observerRemoved",mutation:this,observer:t})}optionalRemove(){this.#t.length||(this.state.status==="pending"?this.scheduleGc():this.#n.remove(this))}continue(){return this.#i?.continue()??this.execute(this.state.variables)}async execute(t){const e=()=>{this.#r({type:"continue"})},n={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};this.#i=OP({fn:()=>this.options.mutationFn?this.options.mutationFn(t,n):Promise.reject(new Error("No mutationFn found")),onFail:(s,a)=>{this.#r({type:"failed",failureCount:s,error:a})},onPause:()=>{this.#r({type:"pause"})},onContinue:e,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>this.#n.canRun(this)});const r=this.state.status==="pending",i=!this.#i.canStart();try{if(r)e();else{this.#r({type:"pending",variables:t,isPaused:i}),await this.#n.config.onMutate?.(t,this,n);const a=await this.options.onMutate?.(t,n);a!==this.state.context&&this.#r({type:"pending",context:a,variables:t,isPaused:i})}const s=await this.#i.start();return await this.#n.config.onSuccess?.(s,t,this.state.context,this,n),await this.options.onSuccess?.(s,t,this.state.context,n),await this.#n.config.onSettled?.(s,null,this.state.variables,this.state.context,this,n),await this.options.onSettled?.(s,null,t,this.state.context,n),this.#r({type:"success",data:s}),s}catch(s){try{throw await this.#n.config.onError?.(s,t,this.state.context,this,n),await this.options.onError?.(s,t,this.state.context,n),await this.#n.config.onSettled?.(void 0,s,this.state.variables,this.state.context,this,n),await this.options.onSettled?.(void 0,s,t,this.state.context,n),s}finally{this.#r({type:"error",error:s})}}finally{this.#n.runNext(this)}}#r(t){const e=n=>{switch(t.type){case"failed":return{...n,failureCount:t.failureCount,failureReason:t.error};case"pause":return{...n,isPaused:!0};case"continue":return{...n,isPaused:!1};case"pending":return{...n,context:t.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:t.isPaused,status:"pending",variables:t.variables,submittedAt:Date.now()};case"success":return{...n,data:t.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...n,data:void 0,error:t.error,failureCount:n.failureCount+1,failureReason:t.error,isPaused:!1,status:"error"}}};this.state=e(this.state),xn.batch(()=>{this.#t.forEach(n=>{n.onMutationUpdate(t)}),this.#n.notify({mutation:this,type:"updated",action:t})})}};function CP(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var M2=class extends mu{constructor(t={}){super(),this.config=t,this.#e=new Set,this.#t=new Map,this.#n=0}#e;#t;#n;build(t,e,n){const r=new b2({client:t,mutationCache:this,mutationId:++this.#n,options:t.defaultMutationOptions(e),state:n});return this.add(r),r}add(t){this.#e.add(t);const e=dE(t);if(typeof e=="string"){const n=this.#t.get(e);n?n.push(t):this.#t.set(e,[t])}this.notify({type:"added",mutation:t})}remove(t){if(this.#e.delete(t)){const e=dE(t);if(typeof e=="string"){const n=this.#t.get(e);if(n)if(n.length>1){const r=n.indexOf(t);r!==-1&&n.splice(r,1)}else n[0]===t&&this.#t.delete(e)}}this.notify({type:"removed",mutation:t})}canRun(t){const e=dE(t);if(typeof e=="string"){const r=this.#t.get(e)?.find(i=>i.state.status==="pending");return!r||r===t}else return!0}runNext(t){const e=dE(t);return typeof e=="string"?this.#t.get(e)?.find(r=>r!==t&&r.state.isPaused)?.continue()??Promise.resolve():Promise.resolve()}clear(){xn.batch(()=>{this.#e.forEach(t=>{this.notify({type:"removed",mutation:t})}),this.#e.clear(),this.#t.clear()})}getAll(){return Array.from(this.#e)}find(t){const e={exact:!0,...t};return this.getAll().find(n=>bv(e,n))}findAll(t={}){return this.getAll().filter(e=>bv(t,e))}notify(t){xn.batch(()=>{this.listeners.forEach(e=>{e(t)})})}resumePausedMutations(){const t=this.getAll().filter(e=>e.state.isPaused);return xn.batch(()=>Promise.all(t.map(e=>e.continue().catch(dr))))}};function dE(t){return t.options.scope?.id}var P2=class extends mu{#e;#t=void 0;#n;#i;constructor(e,n){super(),this.#e=e,this.setOptions(n),this.bindMethods(),this.#r()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){const n=this.options;this.options=this.#e.defaultMutationOptions(e),LT(this.options,n)||this.#e.getMutationCache().notify({type:"observerOptionsUpdated",mutation:this.#n,observer:this}),n?.mutationKey&&this.options.mutationKey&&Rl(n.mutationKey)!==Rl(this.options.mutationKey)?this.reset():this.#n?.state.status==="pending"&&this.#n.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#n?.removeObserver(this)}onMutationUpdate(e){this.#r(),this.#o(e)}getCurrentResult(){return this.#t}reset(){this.#n?.removeObserver(this),this.#n=void 0,this.#r(),this.#o()}mutate(e,n){return this.#i=n,this.#n?.removeObserver(this),this.#n=this.#e.getMutationCache().build(this.#e,this.options),this.#n.addObserver(this),this.#n.execute(e)}#r(){const e=this.#n?.state??CP();this.#t={...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset}}#o(e){xn.batch(()=>{if(this.#i&&this.hasListeners()){const n=this.#t.variables,r=this.#t.context,i={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};e?.type==="success"?(this.#i.onSuccess?.(e.data,n,r,i),this.#i.onSettled?.(e.data,null,n,r,i)):e?.type==="error"&&(this.#i.onError?.(e.error,n,r,i),this.#i.onSettled?.(void 0,e.error,n,r,i))}this.listeners.forEach(n=>{n(this.#t)})})}},x2=class extends mu{constructor(t={}){super(),this.config=t,this.#e=new Map}#e;build(t,e,n){const r=e.queryKey,i=e.queryHash??LN(r,e);let s=this.get(i);return s||(s=new _2({client:t,queryKey:r,queryHash:i,options:t.defaultQueryOptions(e),state:n,defaultOptions:t.getQueryDefaults(r)}),this.add(s)),s}add(t){this.#e.has(t.queryHash)||(this.#e.set(t.queryHash,t),this.notify({type:"added",query:t}))}remove(t){const e=this.#e.get(t.queryHash);e&&(t.destroy(),e===t&&this.#e.delete(t.queryHash),this.notify({type:"removed",query:t}))}clear(){xn.batch(()=>{this.getAll().forEach(t=>{this.remove(t)})})}get(t){return this.#e.get(t)}getAll(){return[...this.#e.values()]}find(t){const e={exact:!0,...t};return this.getAll().find(n=>Dv(e,n))}findAll(t={}){const e=this.getAll();return Object.keys(t).length>0?e.filter(n=>Dv(t,n)):e}notify(t){xn.batch(()=>{this.listeners.forEach(e=>{e(t)})})}onFocus(){xn.batch(()=>{this.getAll().forEach(t=>{t.onFocus()})})}onOnline(){xn.batch(()=>{this.getAll().forEach(t=>{t.onOnline()})})}},w2=class{#e;#t;#n;#i;#r;#o;#a;#s;constructor(t={}){this.#e=t.queryCache||new x2,this.#t=t.mutationCache||new M2,this.#n=t.defaultOptions||{},this.#i=new Map,this.#r=new Map,this.#o=0}mount(){this.#o++,this.#o===1&&(this.#a=vN.subscribe(async t=>{t&&(await this.resumePausedMutations(),this.#e.onFocus())}),this.#s=yT.subscribe(async t=>{t&&(await this.resumePausedMutations(),this.#e.onOnline())}))}unmount(){this.#o--,this.#o===0&&(this.#a?.(),this.#a=void 0,this.#s?.(),this.#s=void 0)}isFetching(t){return this.#e.findAll({...t,fetchStatus:"fetching"}).length}isMutating(t){return this.#t.findAll({...t,status:"pending"}).length}getQueryData(t){const e=this.defaultQueryOptions({queryKey:t});return this.#e.get(e.queryHash)?.state.data}ensureQueryData(t){const e=this.defaultQueryOptions(t),n=this.#e.build(this,e),r=n.state.data;return r===void 0?this.fetchQuery(t):(t.revalidateIfStale&&n.isStaleByTime(da(e.staleTime,n))&&this.prefetchQuery(e),Promise.resolve(r))}getQueriesData(t){return this.#e.findAll(t).map(({queryKey:e,state:n})=>{const r=n.data;return[e,r]})}setQueryData(t,e,n){const r=this.defaultQueryOptions({queryKey:t}),s=this.#e.get(r.queryHash)?.state.data,a=p2(e,s);if(a!==void 0)return this.#e.build(this,r).setData(a,{...n,manual:!0})}setQueriesData(t,e,n){return xn.batch(()=>this.#e.findAll(t).map(({queryKey:r})=>[r,this.setQueryData(r,e,n)]))}getQueryState(t){const e=this.defaultQueryOptions({queryKey:t});return this.#e.get(e.queryHash)?.state}removeQueries(t){const e=this.#e;xn.batch(()=>{e.findAll(t).forEach(n=>{e.remove(n)})})}resetQueries(t,e){const n=this.#e;return xn.batch(()=>(n.findAll(t).forEach(r=>{r.reset()}),this.refetchQueries({type:"active",...t},e)))}cancelQueries(t,e={}){const n={revert:!0,...e},r=xn.batch(()=>this.#e.findAll(t).map(i=>i.cancel(n)));return Promise.all(r).then(dr).catch(dr)}invalidateQueries(t,e={}){return xn.batch(()=>(this.#e.findAll(t).forEach(n=>{n.invalidate()}),t?.refetchType==="none"?Promise.resolve():this.refetchQueries({...t,type:t?.refetchType??t?.type??"active"},e)))}refetchQueries(t,e={}){const n={...e,cancelRefetch:e.cancelRefetch??!0},r=xn.batch(()=>this.#e.findAll(t).filter(i=>!i.isDisabled()&&!i.isStatic()).map(i=>{let s=i.fetch(void 0,n);return n.throwOnError||(s=s.catch(dr)),i.state.fetchStatus==="paused"?Promise.resolve():s}));return Promise.all(r).then(dr)}fetchQuery(t){const e=this.defaultQueryOptions(t);e.retry===void 0&&(e.retry=!1);const n=this.#e.build(this,e);return n.isStaleByTime(da(e.staleTime,n))?n.fetch(e):Promise.resolve(n.state.data)}prefetchQuery(t){return this.fetchQuery(t).then(dr).catch(dr)}fetchInfiniteQuery(t){return t.behavior=Bv(t.pages),this.fetchQuery(t)}prefetchInfiniteQuery(t){return this.fetchInfiniteQuery(t).then(dr).catch(dr)}ensureInfiniteQueryData(t){return t.behavior=Bv(t.pages),this.ensureQueryData(t)}resumePausedMutations(){return yT.isOnline()?this.#t.resumePausedMutations():Promise.resolve()}getQueryCache(){return this.#e}getMutationCache(){return this.#t}getDefaultOptions(){return this.#n}setDefaultOptions(t){this.#n=t}setQueryDefaults(t,e){this.#i.set(Rl(t),{queryKey:t,defaultOptions:e})}getQueryDefaults(t){const e=[...this.#i.values()],n={};return e.forEach(r=>{Zf(t,r.queryKey)&&Object.assign(n,r.defaultOptions)}),n}setMutationDefaults(t,e){this.#r.set(Rl(t),{mutationKey:t,defaultOptions:e})}getMutationDefaults(t){const e=[...this.#r.values()],n={};return e.forEach(r=>{Zf(t,r.mutationKey)&&Object.assign(n,r.defaultOptions)}),n}defaultQueryOptions(t){if(t._defaulted)return t;const e={...this.#n.queries,...this.getQueryDefaults(t.queryKey),...t,_defaulted:!0};return e.queryHash||(e.queryHash=LN(e.queryKey,e)),e.refetchOnReconnect===void 0&&(e.refetchOnReconnect=e.networkMode!=="always"),e.throwOnError===void 0&&(e.throwOnError=!!e.suspense),!e.networkMode&&e.persister&&(e.networkMode="offlineFirst"),e.queryFn===yN&&(e.enabled=!1),e}defaultMutationOptions(t){return t?._defaulted?t:{...this.#n.mutations,...t?.mutationKey&&this.getMutationDefaults(t.mutationKey),...t,_defaulted:!0}}clear(){this.#e.clear(),this.#t.clear()}},_P=O.createContext(void 0),Ls=t=>{const e=O.useContext(_P);if(!e)throw new Error("No QueryClient set, use QueryClientProvider to set one");return e},U2=({client:t,children:e})=>(O.useEffect(()=>(t.mount(),()=>{t.unmount()}),[t]),m.jsx(_P.Provider,{value:t,children:e})),LP=O.createContext(!1),B2=()=>O.useContext(LP);LP.Provider;function G2(){let t=!1;return{clearReset:()=>{t=!1},reset:()=>{t=!0},isReset:()=>t}}var H2=O.createContext(G2()),F2=()=>O.useContext(H2),k2=(t,e)=>{(t.suspense||t.throwOnError||t.experimental_prefetchInRender)&&(e.isReset()||(t.retryOnMount=!1))},V2=t=>{O.useEffect(()=>{t.clearReset()},[t])},Y2=({result:t,errorResetBoundary:e,throwOnError:n,query:r,suspense:i})=>t.isError&&!e.isReset()&&!t.isFetching&&r&&(i&&t.data===void 0||AP(n,[t.error,r])),W2=t=>{if(t.suspense){const n=i=>i==="static"?i:Math.max(i??1e3,1e3),r=t.staleTime;t.staleTime=typeof r=="function"?(...i)=>n(r(...i)):n(r),typeof t.gcTime=="number"&&(t.gcTime=Math.max(t.gcTime,1e3))}},$2=(t,e)=>t.isLoading&&t.isFetching&&!e,z2=(t,e)=>t?.suspense&&e.isPending,Hv=(t,e,n)=>e.fetchOptimistic(t).catch(()=>{n.clearReset()});function j2(t,e,n){const r=B2(),i=F2(),s=Ls(),a=s.defaultQueryOptions(t);s.getDefaultOptions().queries?._experimental_beforeQuery?.(a),a._optimisticResults=r?"isRestoring":"optimistic",W2(a),k2(a,i),V2(i);const c=!s.getQueryCache().get(a.queryHash),[u]=O.useState(()=>new e(s,a)),d=u.getOptimisticResult(a),h=!r&&t.subscribed!==!1;if(O.useSyncExternalStore(O.useCallback(T=>{const p=h?u.subscribe(xn.batchCalls(T)):dr;return u.updateResult(),p},[u,h]),()=>u.getCurrentResult(),()=>u.getCurrentResult()),O.useEffect(()=>{u.setOptions(a)},[a,u]),z2(a,d))throw Hv(a,u,i);if(Y2({result:d,errorResetBoundary:i,throwOnError:a.throwOnError,query:s.getQueryCache().get(a.queryHash),suspense:a.suspense}))throw d.error;return s.getDefaultOptions().queries?._experimental_afterQuery?.(a,d),a.experimental_prefetchInRender&&!pl&&$2(d,r)&&(c?Hv(a,u,i):s.getQueryCache().get(a.queryHash)?.promise)?.catch(dr).finally(()=>{u.updateResult()}),a.notifyOnChangeProps?d:u.trackResult(d)}function bp(t,e){return j2(t,L2)}function _a(t,e){const n=Ls(),[r]=O.useState(()=>new P2(n,t));O.useEffect(()=>{r.setOptions(t)},[r,t]);const i=O.useSyncExternalStore(O.useCallback(a=>r.subscribe(xn.batchCalls(a)),[r]),()=>r.getCurrentResult(),()=>r.getCurrentResult()),s=O.useCallback((a,c)=>{r.mutate(a,c).catch(dr)},[r]);if(i.error&&AP(r.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:s,mutateAsync:i.mutate}}/** + * react-router v7.9.4 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var yP=t=>{throw TypeError(t)},X2=(t,e,n)=>e.has(t)||yP("Cannot "+n),$m=(t,e,n)=>(X2(t,e,"read from private field"),n?n.call(t):e.get(t)),K2=(t,e,n)=>e.has(t)?yP("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n),Fv="popstate";function q2(t={}){function e(r,i){let{pathname:s,search:a,hash:c}=r.location;return ed("",{pathname:s,search:a,hash:c},i.state&&i.state.usr||null,i.state&&i.state.key||"default")}function n(r,i){return typeof i=="string"?i:pa(i)}return Q2(e,n,null,t)}function At(t,e){if(t===!1||t===null||typeof t>"u")throw new Error(e)}function Sn(t,e){if(!t){typeof console<"u"&&console.warn(e);try{throw new Error(e)}catch{}}}function J2(){return Math.random().toString(36).substring(2,10)}function kv(t,e){return{usr:t.state,key:t.key,idx:e}}function ed(t,e,n=null,r){return{pathname:typeof t=="string"?t:t.pathname,search:"",hash:"",...typeof e=="string"?La(e):e,state:n,key:e&&e.key||r||J2()}}function pa({pathname:t="/",search:e="",hash:n=""}){return e&&e!=="?"&&(t+=e.charAt(0)==="?"?e:"?"+e),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function La(t){let e={};if(t){let n=t.indexOf("#");n>=0&&(e.hash=t.substring(n),t=t.substring(0,n));let r=t.indexOf("?");r>=0&&(e.search=t.substring(r),t=t.substring(0,r)),t&&(e.pathname=t)}return e}function Q2(t,e,n,r={}){let{window:i=document.defaultView,v5Compat:s=!1}=r,a=i.history,c="POP",u=null,d=h();d==null&&(d=0,a.replaceState({...a.state,idx:d},""));function h(){return(a.state||{idx:null}).idx}function T(){c="POP";let I=h(),C=I==null?null:I-d;d=I,u&&u({action:c,location:N.location,delta:C})}function p(I,C){c="PUSH";let L=ed(N.location,I,C);d=h()+1;let y=kv(L,d),v=N.createHref(L);try{a.pushState(y,"",v)}catch(b){if(b instanceof DOMException&&b.name==="DataCloneError")throw b;i.location.assign(v)}s&&u&&u({action:c,location:N.location,delta:1})}function R(I,C){c="REPLACE";let L=ed(N.location,I,C);d=h();let y=kv(L,d),v=N.createHref(L);a.replaceState(y,"",v),s&&u&&u({action:c,location:N.location,delta:0})}function g(I){return vP(I)}let N={get action(){return c},get location(){return t(i,a)},listen(I){if(u)throw new Error("A history only accepts one active listener");return i.addEventListener(Fv,T),u=I,()=>{i.removeEventListener(Fv,T),u=null}},createHref(I){return e(i,I)},createURL:g,encodeLocation(I){let C=g(I);return{pathname:C.pathname,search:C.search,hash:C.hash}},push:p,replace:R,go(I){return a.go(I)}};return N}function vP(t,e=!1){let n="http://localhost";typeof window<"u"&&(n=window.location.origin!=="null"?window.location.origin:window.location.href),At(n,"No window.location.(origin|href) available to create URL");let r=typeof t=="string"?t:pa(t);return r=r.replace(/ $/,"%20"),!e&&r.startsWith("//")&&(r=n+r),new URL(r,n)}var Pf,Vv=class{constructor(t){if(K2(this,Pf,new Map),t)for(let[e,n]of t)this.set(e,n)}get(t){if($m(this,Pf).has(t))return $m(this,Pf).get(t);if(t.defaultValue!==void 0)return t.defaultValue;throw new Error("No value found for context")}set(t,e){$m(this,Pf).set(t,e)}};Pf=new WeakMap;var Z2=new Set(["lazy","caseSensitive","path","id","index","children"]);function eW(t){return Z2.has(t)}var tW=new Set(["lazy","caseSensitive","path","id","index","middleware","children"]);function nW(t){return tW.has(t)}function rW(t){return t.index===!0}function td(t,e,n=[],r={},i=!1){return t.map((s,a)=>{let c=[...n,String(a)],u=typeof s.id=="string"?s.id:c.join("-");if(At(s.index!==!0||!s.children,"Cannot specify children on an index route"),At(i||!r[u],`Found a route id collision on id "${u}". Route id's must be globally unique within Data Router usages`),rW(s)){let d={...s,...e(s),id:u};return r[u]=d,d}else{let d={...s,...e(s),id:u,children:void 0};return r[u]=d,s.children&&(d.children=td(s.children,e,c,r,i)),d}})}function na(t,e,n="/"){return iT(t,e,n,!1)}function iT(t,e,n,r){let i=typeof e=="string"?La(e):e,s=br(i.pathname||"/",n);if(s==null)return null;let a=DP(t);sW(a);let c=null;for(let u=0;c==null&&u{let h={relativePath:d===void 0?a.path||"":d,caseSensitive:a.caseSensitive===!0,childrenIndex:c,route:a};if(h.relativePath.startsWith("/")){if(!h.relativePath.startsWith(r)&&u)return;At(h.relativePath.startsWith(r),`Absolute route path "${h.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),h.relativePath=h.relativePath.slice(r.length)}let T=ps([r,h.relativePath]),p=n.concat(h);a.children&&a.children.length>0&&(At(a.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${T}".`),DP(a.children,e,p,T,u)),!(a.path==null&&!a.index)&&e.push({path:T,score:dW(T,a.index),routesMeta:p})};return t.forEach((a,c)=>{if(a.path===""||!a.path?.includes("?"))s(a,c);else for(let u of bP(a.path))s(a,c,!0,u)}),e}function bP(t){let e=t.split("/");if(e.length===0)return[];let[n,...r]=e,i=n.endsWith("?"),s=n.replace(/\?$/,"");if(r.length===0)return i?[s,""]:[s];let a=bP(r.join("/")),c=[];return c.push(...a.map(u=>u===""?s:[s,u].join("/"))),i&&c.push(...a),c.map(u=>t.startsWith("/")&&u===""?"/":u)}function sW(t){t.sort((e,n)=>e.score!==n.score?n.score-e.score:hW(e.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var oW=/^:[\w-]+$/,aW=3,lW=2,cW=1,uW=10,fW=-2,Yv=t=>t==="*";function dW(t,e){let n=t.split("/"),r=n.length;return n.some(Yv)&&(r+=fW),e&&(r+=lW),n.filter(i=>!Yv(i)).reduce((i,s)=>i+(oW.test(s)?aW:s===""?cW:uW),r)}function hW(t,e){return t.length===e.length&&t.slice(0,-1).every((r,i)=>r===e[i])?t[t.length-1]-e[e.length-1]:0}function EW(t,e,n=!1){let{routesMeta:r}=t,i={},s="/",a=[];for(let c=0;c{if(h==="*"){let g=c[p]||"";a=s.slice(0,s.length-g.length).replace(/(.)\/+$/,"$1")}const R=c[p];return T&&!R?d[h]=void 0:d[h]=(R||"").replace(/%2F/g,"/"),d},{}),pathname:s,pathnameBase:a,pattern:t}}function TW(t,e=!1,n=!0){Sn(t==="*"||!t.endsWith("*")||t.endsWith("/*"),`Route path "${t}" will be treated as if it were "${t.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${t.replace(/\*$/,"/*")}".`);let r=[],i="^"+t.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(a,c,u)=>(r.push({paramName:c,isOptional:u!=null}),u?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return t.endsWith("*")?(r.push({paramName:"*"}),i+=t==="*"||t==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?i+="\\/*$":t!==""&&t!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,e?void 0:"i"),r]}function pW(t){try{return t.split("/").map(e=>decodeURIComponent(e).replace(/\//g,"%2F")).join("/")}catch(e){return Sn(!1,`The URL path "${t}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${e}).`),t}}function br(t,e){if(e==="/")return t;if(!t.toLowerCase().startsWith(e.toLowerCase()))return null;let n=e.endsWith("/")?e.length-1:e.length,r=t.charAt(n);return r&&r!=="/"?null:t.slice(n)||"/"}function RW({basename:t,pathname:e}){return e==="/"?t:ps([t,e])}function SW(t,e="/"){let{pathname:n,search:r="",hash:i=""}=typeof t=="string"?La(t):t;return{pathname:n?n.startsWith("/")?n:mW(n,e):e,search:gW(r),hash:OW(i)}}function mW(t,e){let n=e.replace(/\/+$/,"").split("/");return t.split("/").forEach(i=>{i===".."?n.length>1&&n.pop():i!=="."&&n.push(i)}),n.length>1?n.join("/"):"/"}function zm(t,e,n,r){return`Cannot include a '${t}' character in a manually specified \`to.${e}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function MP(t){return t.filter((e,n)=>n===0||e.route.path&&e.route.path.length>0)}function bN(t){let e=MP(t);return e.map((n,r)=>r===e.length-1?n.pathname:n.pathnameBase)}function MN(t,e,n,r=!1){let i;typeof t=="string"?i=La(t):(i={...t},At(!i.pathname||!i.pathname.includes("?"),zm("?","pathname","search",i)),At(!i.pathname||!i.pathname.includes("#"),zm("#","pathname","hash",i)),At(!i.search||!i.search.includes("#"),zm("#","search","hash",i)));let s=t===""||i.pathname==="",a=s?"/":i.pathname,c;if(a==null)c=n;else{let T=e.length-1;if(!r&&a.startsWith("..")){let p=a.split("/");for(;p[0]==="..";)p.shift(),T-=1;i.pathname=p.join("/")}c=T>=0?e[T]:"/"}let u=SW(i,c),d=a&&a!=="/"&&a.endsWith("/"),h=(s||a===".")&&n.endsWith("/");return!u.pathname.endsWith("/")&&(d||h)&&(u.pathname+="/"),u}var ps=t=>t.join("/").replace(/\/\/+/g,"/"),AW=t=>t.replace(/\/+$/,"").replace(/^\/*/,"/"),gW=t=>!t||t==="?"?"":t.startsWith("?")?t:"?"+t,OW=t=>!t||t==="#"?"":t.startsWith("#")?t:"#"+t,DT=class{constructor(t,e,n,r=!1){this.status=t,this.statusText=e||"",this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function nd(t){return t!=null&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.internal=="boolean"&&"data"in t}var PP=["POST","PUT","PATCH","DELETE"],NW=new Set(PP),IW=["GET",...PP],CW=new Set(IW),_W=new Set([301,302,303,307,308]),LW=new Set([307,308]),jm={state:"idle",location:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},yW={state:"idle",data:void 0,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0},yc={state:"unblocked",proceed:void 0,reset:void 0,location:void 0},vW=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,PN=t=>vW.test(t),DW=t=>({hasErrorBoundary:!!t.hasErrorBoundary}),xP="remix-router-transitions",wP=Symbol("ResetLoaderData");function bW(t){const e=t.window?t.window:typeof window<"u"?window:void 0,n=typeof e<"u"&&typeof e.document<"u"&&typeof e.document.createElement<"u";At(t.routes.length>0,"You must provide a non-empty routes array to createRouter");let r=t.hydrationRouteProperties||[],i=t.mapRouteProperties||DW,s={},a=td(t.routes,i,void 0,s),c,u=t.basename||"/";u.startsWith("/")||(u=`/${u}`);let d=t.dataStrategy||UW,h={...t.future},T=null,p=new Set,R=null,g=null,N=null,I=t.hydrationData!=null,C=na(a,t.history.location,u),L=!1,y=null,v;if(C==null&&!t.patchRoutesOnNavigation){let G=Ei(404,{pathname:t.history.location.pathname}),{matches:$,route:le}=hE(a);v=!0,C=$,y={[le.id]:G}}else if(C&&!t.hydrationData&&qi(C,a,t.history.location.pathname).active&&(C=null),C)if(C.some(G=>G.route.lazy))v=!1;else if(!C.some(G=>xN(G.route)))v=!0;else{let G=t.hydrationData?t.hydrationData.loaderData:null,$=t.hydrationData?t.hydrationData.errors:null;if($){let le=C.findIndex(Ne=>$[Ne.route.id]!==void 0);v=C.slice(0,le+1).every(Ne=>!Hg(Ne.route,G,$))}else v=C.every(le=>!Hg(le.route,G,$))}else{v=!1,C=[];let G=qi(null,a,t.history.location.pathname);G.active&&G.matches&&(L=!0,C=G.matches)}let b,D={historyAction:t.history.action,location:t.history.location,matches:C,initialized:v,navigation:jm,restoreScrollPosition:t.hydrationData!=null?!1:null,preventScrollReset:!1,revalidation:"idle",loaderData:t.hydrationData&&t.hydrationData.loaderData||{},actionData:t.hydrationData&&t.hydrationData.actionData||null,errors:t.hydrationData&&t.hydrationData.errors||y,fetchers:new Map,blockers:new Map},M="POP",w=!1,F,Q=!1,ne=new Map,z=null,ie=!1,se=!1,Ee=new Set,U=new Map,Z=0,V=-1,oe=new Map,ge=new Set,x=new Map,X=new Map,Y=new Set,re=new Map,Re,me=null;function q(){if(T=t.history.listen(({action:G,location:$,delta:le})=>{if(Re){Re(),Re=void 0;return}Sn(re.size===0||le!=null,"You are trying to use a blocker on a POP navigation to a location that was not created by @remix-run/router. This will fail silently in production. This can happen if you are navigating outside the router via `window.history.pushState`/`window.location.hash` instead of using router navigation APIs. This can also happen if you are using createHashRouter and the user manually changes the URL.");let Ne=eh({currentLocation:D.location,nextLocation:$,historyAction:G});if(Ne&&le!=null){let ye=new Promise(ze=>{Re=ze});t.history.go(le*-1),xs(Ne,{state:"blocked",location:$,proceed(){xs(Ne,{state:"proceeding",proceed:void 0,reset:void 0,location:$}),ye.then(()=>t.history.go(le))},reset(){let ze=new Map(D.blockers);ze.set(Ne,yc),He({blockers:ze})}});return}return dt(G,$)}),n){JW(e,ne);let G=()=>QW(e,ne);e.addEventListener("pagehide",G),z=()=>e.removeEventListener("pagehide",G)}return D.initialized||dt("POP",D.location,{initialHydration:!0}),b}function Ce(){T&&T(),z&&z(),p.clear(),F&&F.abort(),D.fetchers.forEach((G,$)=>vi($)),D.blockers.forEach((G,$)=>Bl($))}function Le(G){return p.add(G),()=>p.delete(G)}function He(G,$={}){G.matches&&(G.matches=G.matches.map(ye=>{let ze=s[ye.route.id],Xe=ye.route;return Xe.element!==ze.element||Xe.errorElement!==ze.errorElement||Xe.hydrateFallbackElement!==ze.hydrateFallbackElement?{...ye,route:ze}:ye})),D={...D,...G};let le=[],Ne=[];D.fetchers.forEach((ye,ze)=>{ye.state==="idle"&&(Y.has(ze)?le.push(ze):Ne.push(ze))}),Y.forEach(ye=>{!D.fetchers.has(ye)&&!U.has(ye)&&le.push(ye)}),[...p].forEach(ye=>ye(D,{deletedFetchers:le,viewTransitionOpts:$.viewTransitionOpts,flushSync:$.flushSync===!0})),le.forEach(ye=>vi(ye)),Ne.forEach(ye=>D.fetchers.delete(ye))}function ot(G,$,{flushSync:le}={}){let Ne=D.actionData!=null&&D.navigation.formMethod!=null&&Cr(D.navigation.formMethod)&&D.navigation.state==="loading"&&G.state?._isRedirect!==!0,ye;$.actionData?Object.keys($.actionData).length>0?ye=$.actionData:ye=null:Ne?ye=D.actionData:ye=null;let ze=$.loaderData?Zv(D.loaderData,$.loaderData,$.matches||[],$.errors):D.loaderData,Xe=D.blockers;Xe.size>0&&(Xe=new Map(Xe),Xe.forEach((we,Ue)=>Xe.set(Ue,yc)));let Ye=ie?!1:Ma(G,$.matches||D.matches),Ke=w===!0||D.navigation.formMethod!=null&&Cr(D.navigation.formMethod)&&G.state?._isRedirect!==!0;c&&(a=c,c=void 0),ie||M==="POP"||(M==="PUSH"?t.history.push(G,G.state):M==="REPLACE"&&t.history.replace(G,G.state));let qe;if(M==="POP"){let we=ne.get(D.location.pathname);we&&we.has(G.pathname)?qe={currentLocation:D.location,nextLocation:G}:ne.has(G.pathname)&&(qe={currentLocation:G,nextLocation:D.location})}else if(Q){let we=ne.get(D.location.pathname);we?we.add(G.pathname):(we=new Set([G.pathname]),ne.set(D.location.pathname,we)),qe={currentLocation:D.location,nextLocation:G}}He({...$,actionData:ye,loaderData:ze,historyAction:M,location:G,initialized:!0,navigation:jm,revalidation:"idle",restoreScrollPosition:Ye,preventScrollReset:Ke,blockers:Xe},{viewTransitionOpts:qe,flushSync:le===!0}),M="POP",w=!1,Q=!1,ie=!1,se=!1,me?.resolve(),me=null}async function it(G,$){if(typeof G=="number"){t.history.go(G);return}let le=Gg(D.location,D.matches,u,G,$?.fromRouteId,$?.relative),{path:Ne,submission:ye,error:ze}=Wv(!1,le,$),Xe=D.location,Ye=ed(D.location,Ne,$&&$.state);Ye={...Ye,...t.history.encodeLocation(Ye)};let Ke=$&&$.replace!=null?$.replace:void 0,qe="PUSH";Ke===!0?qe="REPLACE":Ke===!1||ye!=null&&Cr(ye.formMethod)&&ye.formAction===D.location.pathname+D.location.search&&(qe="REPLACE");let we=$&&"preventScrollReset"in $?$.preventScrollReset===!0:void 0,Ue=($&&$.flushSync)===!0,ht=eh({currentLocation:Xe,nextLocation:Ye,historyAction:qe});if(ht){xs(ht,{state:"blocked",location:Ye,proceed(){xs(ht,{state:"proceeding",proceed:void 0,reset:void 0,location:Ye}),it(G,$)},reset(){let Ht=new Map(D.blockers);Ht.set(ht,yc),He({blockers:Ht})}});return}await dt(qe,Ye,{submission:ye,pendingError:ze,preventScrollReset:we,replace:$&&$.replace,enableViewTransition:$&&$.viewTransition,flushSync:Ue})}function Ze(){me||(me=ZW()),Ie(),He({revalidation:"loading"});let G=me.promise;return D.navigation.state==="submitting"?G:D.navigation.state==="idle"?(dt(D.historyAction,D.location,{startUninterruptedRevalidation:!0}),G):(dt(M||D.historyAction,D.navigation.location,{overrideNavigation:D.navigation,enableViewTransition:Q===!0}),G)}async function dt(G,$,le){F&&F.abort(),F=null,M=G,ie=(le&&le.startUninterruptedRevalidation)===!0,ws(D.location,D.matches),w=(le&&le.preventScrollReset)===!0,Q=(le&&le.enableViewTransition)===!0;let Ne=c||a,ye=le&&le.overrideNavigation,ze=le?.initialHydration&&D.matches&&D.matches.length>0&&!L?D.matches:na(Ne,$,u),Xe=(le&&le.flushSync)===!0;if(ze&&D.initialized&&!se&&WW(D.location,$)&&!(le&&le.submission&&Cr(le.submission.formMethod))){ot($,{matches:ze},{flushSync:Xe});return}let Ye=qi(ze,Ne,$.pathname);if(Ye.active&&Ye.matches&&(ze=Ye.matches),!ze){let{error:$n,notFoundMatches:Kt,route:Pt}=Lo($.pathname);ot($,{matches:Kt,loaderData:{},errors:{[Pt.id]:$n}},{flushSync:Xe});return}F=new AbortController;let Ke=vc(t.history,$,F.signal,le&&le.submission),qe=t.getContext?await t.getContext():new Vv,we;if(le&&le.pendingError)we=[ra(ze).route.id,{type:"error",error:le.pendingError}];else if(le&&le.submission&&Cr(le.submission.formMethod)){let $n=await Ct(Ke,$,le.submission,ze,qe,Ye.active,le&&le.initialHydration===!0,{replace:le.replace,flushSync:Xe});if($n.shortCircuited)return;if($n.pendingActionResult){let[Kt,Pt]=$n.pendingActionResult;if($r(Pt)&&nd(Pt.error)&&Pt.error.status===404){F=null,ot($,{matches:$n.matches,loaderData:{},errors:{[Kt]:Pt.error}});return}}ze=$n.matches||ze,we=$n.pendingActionResult,ye=Xm($,le.submission),Xe=!1,Ye.active=!1,Ke=vc(t.history,Ke.url,Ke.signal)}let{shortCircuited:Ue,matches:ht,loaderData:Ht,errors:gn}=await cn(Ke,$,ze,qe,Ye.active,ye,le&&le.submission,le&&le.fetcherSubmission,le&&le.replace,le&&le.initialHydration===!0,Xe,we);Ue||(F=null,ot($,{matches:ht||ze,...eD(we),loaderData:Ht,errors:gn}))}async function Ct(G,$,le,Ne,ye,ze,Xe,Ye={}){Ie();let Ke=KW($,le);if(He({navigation:Ke},{flushSync:Ye.flushSync===!0}),ze){let Ue=await Hl(Ne,$.pathname,G.signal);if(Ue.type==="aborted")return{shortCircuited:!0};if(Ue.type==="error"){if(Ue.partialMatches.length===0){let{matches:Ht,route:gn}=hE(a);return{matches:Ht,pendingActionResult:[gn.id,{type:"error",error:Ue.error}]}}let ht=ra(Ue.partialMatches).route.id;return{matches:Ue.partialMatches,pendingActionResult:[ht,{type:"error",error:Ue.error}]}}else if(Ue.matches)Ne=Ue.matches;else{let{notFoundMatches:ht,error:Ht,route:gn}=Lo($.pathname);return{matches:ht,pendingActionResult:[gn.id,{type:"error",error:Ht}]}}}let qe,we=sT(Ne,$);if(!we.route.action&&!we.route.lazy)qe={type:"error",error:Ei(405,{method:G.method,pathname:$.pathname,routeId:we.route.id})};else{let Ue=kc(i,s,G,Ne,we,Xe?[]:r,ye),ht=await ve(G,Ue,ye,null);if(qe=ht[we.route.id],!qe){for(let Ht of Ne)if(ht[Ht.route.id]){qe=ht[Ht.route.id];break}}if(G.signal.aborted)return{shortCircuited:!0}}if(ll(qe)){let Ue;return Ye&&Ye.replace!=null?Ue=Ye.replace:Ue=qv(qe.response.headers.get("Location"),new URL(G.url),u)===D.location.pathname+D.location.search,await ue(G,qe,!0,{submission:le,replace:Ue}),{shortCircuited:!0}}if($r(qe)){let Ue=ra(Ne,we.route.id);return(Ye&&Ye.replace)!==!0&&(M="PUSH"),{matches:Ne,pendingActionResult:[Ue.route.id,qe,we.route.id]}}return{matches:Ne,pendingActionResult:[we.route.id,qe]}}async function cn(G,$,le,Ne,ye,ze,Xe,Ye,Ke,qe,we,Ue){let ht=ze||Xm($,Xe),Ht=Xe||Ye||nD(ht),gn=!ie&&!qe;if(ye){if(gn){let Gn=lr(Ue);He({navigation:ht,...Gn!==void 0?{actionData:Gn}:{}},{flushSync:we})}let Et=await Hl(le,$.pathname,G.signal);if(Et.type==="aborted")return{shortCircuited:!0};if(Et.type==="error"){if(Et.partialMatches.length===0){let{matches:Us,route:Zi}=hE(a);return{matches:Us,loaderData:{},errors:{[Zi.id]:Et.error}}}let Gn=ra(Et.partialMatches).route.id;return{matches:Et.partialMatches,loaderData:{},errors:{[Gn]:Et.error}}}else if(Et.matches)le=Et.matches;else{let{error:Gn,notFoundMatches:Us,route:Zi}=Lo($.pathname);return{matches:Us,loaderData:{},errors:{[Zi.id]:Gn}}}}let $n=c||a,{dsMatches:Kt,revalidatingFetchers:Pt}=$v(G,Ne,i,s,t.history,D,le,Ht,$,qe?[]:r,qe===!0,se,Ee,Y,x,ge,$n,u,t.patchRoutesOnNavigation!=null,Ue);if(V=++Z,!t.dataStrategy&&!Kt.some(Et=>Et.shouldLoad)&&!Kt.some(Et=>Et.route.middleware&&Et.route.middleware.length>0)&&Pt.length===0){let Et=An();return ot($,{matches:le,loaderData:{},errors:Ue&&$r(Ue[1])?{[Ue[0]]:Ue[1].error}:null,...eD(Ue),...Et?{fetchers:new Map(D.fetchers)}:{}},{flushSync:we}),{shortCircuited:!0}}if(gn){let Et={};if(!ye){Et.navigation=ht;let Gn=lr(Ue);Gn!==void 0&&(Et.actionData=Gn)}Pt.length>0&&(Et.fetchers=Xt(Pt)),He(Et,{flushSync:we})}Pt.forEach(Et=>{Mr(Et.key),Et.controller&&U.set(Et.key,Et.controller)});let Di=()=>Pt.forEach(Et=>Mr(Et.key));F&&F.signal.addEventListener("abort",Di);let{loaderResults:ni,fetcherResults:ur}=await Oe(Kt,Pt,G,Ne);if(G.signal.aborted)return{shortCircuited:!0};F&&F.signal.removeEventListener("abort",Di),Pt.forEach(Et=>U.delete(Et.key));let tr=EE(ni);if(tr)return await ue(G,tr.result,!0,{replace:Ke}),{shortCircuited:!0};if(tr=EE(ur),tr)return ge.add(tr.key),await ue(G,tr.result,!0,{replace:Ke}),{shortCircuited:!0};let{loaderData:Ji,errors:rn}=Qv(D,le,ni,Ue,Pt,ur);qe&&D.errors&&(rn={...D.errors,...rn});let Qi=An(),Pa=Pr(V),ri=Qi||Pa||Pt.length>0;return{matches:le,loaderData:Ji,errors:rn,...ri?{fetchers:new Map(D.fetchers)}:{}}}function lr(G){if(G&&!$r(G[1]))return{[G[0]]:G[1].data};if(D.actionData)return Object.keys(D.actionData).length===0?null:D.actionData}function Xt(G){return G.forEach($=>{let le=D.fetchers.get($.key),Ne=Of(void 0,le?le.data:void 0);D.fetchers.set($.key,Ne)}),new Map(D.fetchers)}async function cr(G,$,le,Ne){Mr(G);let ye=(Ne&&Ne.flushSync)===!0,ze=c||a,Xe=Gg(D.location,D.matches,u,le,$,Ne?.relative),Ye=na(ze,Xe,u),Ke=qi(Ye,ze,Xe);if(Ke.active&&Ke.matches&&(Ye=Ke.matches),!Ye){lt(G,$,Ei(404,{pathname:Xe}),{flushSync:ye});return}let{path:qe,submission:we,error:Ue}=Wv(!0,Xe,Ne);if(Ue){lt(G,$,Ue,{flushSync:ye});return}let ht=t.getContext?await t.getContext():new Vv,Ht=(Ne&&Ne.preventScrollReset)===!0;if(we&&Cr(we.formMethod)){await H(G,$,qe,Ye,ht,Ke.active,ye,Ht,we);return}x.set(G,{routeId:$,path:qe}),await j(G,$,qe,Ye,ht,Ke.active,ye,Ht,we)}async function H(G,$,le,Ne,ye,ze,Xe,Ye,Ke){Ie(),x.delete(G);let qe=D.fetchers.get(G);Ge(G,qW(Ke,qe),{flushSync:Xe});let we=new AbortController,Ue=vc(t.history,le,we.signal,Ke);if(ze){let hn=await Hl(Ne,new URL(Ue.url).pathname,Ue.signal,G);if(hn.type==="aborted")return;if(hn.type==="error"){lt(G,$,hn.error,{flushSync:Xe});return}else if(hn.matches)Ne=hn.matches;else{lt(G,$,Ei(404,{pathname:le}),{flushSync:Xe});return}}let ht=sT(Ne,le);if(!ht.route.action&&!ht.route.lazy){let hn=Ei(405,{method:Ke.formMethod,pathname:le,routeId:$});lt(G,$,hn,{flushSync:Xe});return}U.set(G,we);let Ht=Z,gn=kc(i,s,Ue,Ne,ht,r,ye),Kt=(await ve(Ue,gn,ye,G))[ht.route.id];if(Ue.signal.aborted){U.get(G)===we&&U.delete(G);return}if(Y.has(G)){if(ll(Kt)||$r(Kt)){Ge(G,io(void 0));return}}else{if(ll(Kt))if(U.delete(G),V>Ht){Ge(G,io(void 0));return}else return ge.add(G),Ge(G,Of(Ke)),ue(Ue,Kt,!1,{fetcherSubmission:Ke,preventScrollReset:Ye});if($r(Kt)){lt(G,$,Kt.error);return}}let Pt=D.navigation.location||D.location,Di=vc(t.history,Pt,we.signal),ni=c||a,ur=D.navigation.state!=="idle"?na(ni,D.navigation.location,u):D.matches;At(ur,"Didn't find any matches after fetcher action");let tr=++Z;oe.set(G,tr);let Ji=Of(Ke,Kt.data);D.fetchers.set(G,Ji);let{dsMatches:rn,revalidatingFetchers:Qi}=$v(Di,ye,i,s,t.history,D,ur,Ke,Pt,r,!1,se,Ee,Y,x,ge,ni,u,t.patchRoutesOnNavigation!=null,[ht.route.id,Kt]);Qi.filter(hn=>hn.key!==G).forEach(hn=>{let Bs=hn.key,xa=D.fetchers.get(Bs),bi=Of(void 0,xa?xa.data:void 0);D.fetchers.set(Bs,bi),Mr(Bs),hn.controller&&U.set(Bs,hn.controller)}),He({fetchers:new Map(D.fetchers)});let Pa=()=>Qi.forEach(hn=>Mr(hn.key));we.signal.addEventListener("abort",Pa);let{loaderResults:ri,fetcherResults:Et}=await Oe(rn,Qi,Di,ye);if(we.signal.aborted)return;if(we.signal.removeEventListener("abort",Pa),oe.delete(G),U.delete(G),Qi.forEach(hn=>U.delete(hn.key)),D.fetchers.has(G)){let hn=io(Kt.data);D.fetchers.set(G,hn)}let Gn=EE(ri);if(Gn)return ue(Di,Gn.result,!1,{preventScrollReset:Ye});if(Gn=EE(Et),Gn)return ge.add(Gn.key),ue(Di,Gn.result,!1,{preventScrollReset:Ye});let{loaderData:Us,errors:Zi}=Qv(D,ur,ri,void 0,Qi,Et);Pr(tr),D.navigation.state==="loading"&&tr>V?(At(M,"Expected pending action"),F&&F.abort(),ot(D.navigation.location,{matches:ur,loaderData:Us,errors:Zi,fetchers:new Map(D.fetchers)})):(He({errors:Zi,loaderData:Zv(D.loaderData,Us,ur,Zi),fetchers:new Map(D.fetchers)}),se=!1)}async function j(G,$,le,Ne,ye,ze,Xe,Ye,Ke){let qe=D.fetchers.get(G);Ge(G,Of(Ke,qe?qe.data:void 0),{flushSync:Xe});let we=new AbortController,Ue=vc(t.history,le,we.signal);if(ze){let Pt=await Hl(Ne,new URL(Ue.url).pathname,Ue.signal,G);if(Pt.type==="aborted")return;if(Pt.type==="error"){lt(G,$,Pt.error,{flushSync:Xe});return}else if(Pt.matches)Ne=Pt.matches;else{lt(G,$,Ei(404,{pathname:le}),{flushSync:Xe});return}}let ht=sT(Ne,le);U.set(G,we);let Ht=Z,gn=kc(i,s,Ue,Ne,ht,r,ye),Kt=(await ve(Ue,gn,ye,G))[ht.route.id];if(U.get(G)===we&&U.delete(G),!Ue.signal.aborted){if(Y.has(G)){Ge(G,io(void 0));return}if(ll(Kt))if(V>Ht){Ge(G,io(void 0));return}else{ge.add(G),await ue(Ue,Kt,!1,{preventScrollReset:Ye});return}if($r(Kt)){lt(G,$,Kt.error);return}Ge(G,io(Kt.data))}}async function ue(G,$,le,{submission:Ne,fetcherSubmission:ye,preventScrollReset:ze,replace:Xe}={}){$.response.headers.has("X-Remix-Revalidate")&&(se=!0);let Ye=$.response.headers.get("Location");At(Ye,"Expected a Location header on the redirect Response"),Ye=qv(Ye,new URL(G.url),u);let Ke=ed(D.location,Ye,{_isRedirect:!0});if(n){let gn=!1;if($.response.headers.has("X-Remix-Reload-Document"))gn=!0;else if(PN(Ye)){const $n=vP(Ye,!0);gn=$n.origin!==e.location.origin||br($n.pathname,u)==null}if(gn){Xe?e.location.replace(Ye):e.location.assign(Ye);return}}F=null;let qe=Xe===!0||$.response.headers.has("X-Remix-Replace")?"REPLACE":"PUSH",{formMethod:we,formAction:Ue,formEncType:ht}=D.navigation;!Ne&&!ye&&we&&Ue&&ht&&(Ne=nD(D.navigation));let Ht=Ne||ye;if(LW.has($.response.status)&&Ht&&Cr(Ht.formMethod))await dt(qe,Ke,{submission:{...Ht,formAction:Ye},preventScrollReset:ze||w,enableViewTransition:le?Q:void 0});else{let gn=Xm(Ke,Ne);await dt(qe,Ke,{overrideNavigation:gn,fetcherSubmission:ye,preventScrollReset:ze||w,enableViewTransition:le?Q:void 0})}}async function ve(G,$,le,Ne){let ye,ze={};try{ye=await GW(d,G,$,Ne,le,!1)}catch(Xe){return $.filter(Ye=>Ye.shouldLoad).forEach(Ye=>{ze[Ye.route.id]={type:"error",error:Xe}}),ze}if(G.signal.aborted)return ze;for(let[Xe,Ye]of Object.entries(ye))if(jW(Ye)){let Ke=Ye.result;ze[Xe]={type:"redirect",response:VW(Ke,G,Xe,$,u)}}else ze[Xe]=await kW(Ye);return ze}async function Oe(G,$,le,Ne){let ye=ve(le,G,Ne,null),ze=Promise.all($.map(async Ke=>{if(Ke.matches&&Ke.match&&Ke.request&&Ke.controller){let we=(await ve(Ke.request,Ke.matches,Ne,Ke.key))[Ke.match.route.id];return{[Ke.key]:we}}else return Promise.resolve({[Ke.key]:{type:"error",error:Ei(404,{pathname:Ke.path})}})})),Xe=await ye,Ye=(await ze).reduce((Ke,qe)=>Object.assign(Ke,qe),{});return{loaderResults:Xe,fetcherResults:Ye}}function Ie(){se=!0,x.forEach((G,$)=>{U.has($)&&Ee.add($),Mr($)})}function Ge(G,$,le={}){D.fetchers.set(G,$),He({fetchers:new Map(D.fetchers)},{flushSync:(le&&le.flushSync)===!0})}function lt(G,$,le,Ne={}){let ye=ra(D.matches,$);vi(G),He({errors:{[ye.route.id]:le},fetchers:new Map(D.fetchers)},{flushSync:(Ne&&Ne.flushSync)===!0})}function _t(G){return X.set(G,(X.get(G)||0)+1),Y.has(G)&&Y.delete(G),D.fetchers.get(G)||yW}function er(G,$){Mr(G,$?.reason),Ge(G,io(null))}function vi(G){let $=D.fetchers.get(G);U.has(G)&&!($&&$.state==="loading"&&oe.has(G))&&Mr(G),x.delete(G),oe.delete(G),ge.delete(G),Y.delete(G),Ee.delete(G),D.fetchers.delete(G)}function ei(G){let $=(X.get(G)||0)-1;$<=0?(X.delete(G),Y.add(G)):X.set(G,$),He({fetchers:new Map(D.fetchers)})}function Mr(G,$){let le=U.get(G);le&&(le.abort($),U.delete(G))}function ti(G){for(let $ of G){let le=_t($),Ne=io(le.data);D.fetchers.set($,Ne)}}function An(){let G=[],$=!1;for(let le of ge){let Ne=D.fetchers.get(le);At(Ne,`Expected fetcher: ${le}`),Ne.state==="loading"&&(ge.delete(le),G.push(le),$=!0)}return ti(G),$}function Pr(G){let $=[];for(let[le,Ne]of oe)if(Ne0}function Bn(G,$){let le=D.blockers.get(G)||yc;return re.get(G)!==$&&re.set(G,$),le}function Bl(G){D.blockers.delete(G),re.delete(G)}function xs(G,$){let le=D.blockers.get(G)||yc;At(le.state==="unblocked"&&$.state==="blocked"||le.state==="blocked"&&$.state==="blocked"||le.state==="blocked"&&$.state==="proceeding"||le.state==="blocked"&&$.state==="unblocked"||le.state==="proceeding"&&$.state==="unblocked",`Invalid blocker state transition: ${le.state} -> ${$.state}`);let Ne=new Map(D.blockers);Ne.set(G,$),He({blockers:Ne})}function eh({currentLocation:G,nextLocation:$,historyAction:le}){if(re.size===0)return;re.size>1&&Sn(!1,"A router only supports one blocker at a time");let Ne=Array.from(re.entries()),[ye,ze]=Ne[Ne.length-1],Xe=D.blockers.get(ye);if(!(Xe&&Xe.state==="proceeding")&&ze({currentLocation:G,nextLocation:$,historyAction:le}))return ye}function Lo(G){let $=Ei(404,{pathname:G}),le=c||a,{matches:Ne,route:ye}=hE(le);return{notFoundMatches:Ne,route:ye,error:$}}function Gl(G,$,le){if(R=G,N=$,g=le||null,!I&&D.navigation===jm){I=!0;let Ne=Ma(D.location,D.matches);Ne!=null&&He({restoreScrollPosition:Ne})}return()=>{R=null,N=null,g=null}}function ba(G,$){return g&&g(G,$.map(Ne=>iW(Ne,D.loaderData)))||G.key}function ws(G,$){if(R&&N){let le=ba(G,$);R[le]=N()}}function Ma(G,$){if(R){let le=ba(G,$),Ne=R[le];if(typeof Ne=="number")return Ne}return null}function qi(G,$,le){if(t.patchRoutesOnNavigation)if(G){if(Object.keys(G[0].params).length>0)return{active:!0,matches:iT($,le,u,!0)}}else return{active:!0,matches:iT($,le,u,!0)||[]};return{active:!1,matches:null}}async function Hl(G,$,le,Ne){if(!t.patchRoutesOnNavigation)return{type:"success",matches:G};let ye=G;for(;;){let ze=c==null,Xe=c||a,Ye=s;try{await t.patchRoutesOnNavigation({signal:le,path:$,matches:ye,fetcherKey:Ne,patch:(we,Ue)=>{le.aborted||zv(we,Ue,Xe,Ye,i,!1)}})}catch(we){return{type:"error",error:we,partialMatches:ye}}finally{ze&&!le.aborted&&(a=[...a])}if(le.aborted)return{type:"aborted"};let Ke=na(Xe,$,u);if(Ke)return{type:"success",matches:Ke};let qe=iT(Xe,$,u,!0);if(!qe||ye.length===qe.length&&ye.every((we,Ue)=>we.route.id===qe[Ue].route.id))return{type:"success",matches:null};ye=qe}}function th(G){s={},c=td(G,i,void 0,s)}function bu(G,$,le=!1){let Ne=c==null;zv(G,$,c||a,s,i,le),Ne&&(a=[...a],He({}))}return b={get basename(){return u},get future(){return h},get state(){return D},get routes(){return a},get window(){return e},initialize:q,subscribe:Le,enableScrollRestoration:Gl,navigate:it,fetch:cr,revalidate:Ze,createHref:G=>t.history.createHref(G),encodeLocation:G=>t.history.encodeLocation(G),getFetcher:_t,resetFetcher:er,deleteFetcher:ei,dispose:Ce,getBlocker:Bn,deleteBlocker:Bl,patchRoutes:bu,_internalFetchControllers:U,_internalSetRoutes:th,_internalSetStateDoNotUseOrYouWillBreakYourApp(G){He(G)}},b}function MW(t){return t!=null&&("formData"in t&&t.formData!=null||"body"in t&&t.body!==void 0)}function Gg(t,e,n,r,i,s){let a,c;if(i){a=[];for(let d of e)if(a.push(d),d.route.id===i){c=d;break}}else a=e,c=e[e.length-1];let u=MN(r||".",bN(a),br(t.pathname,n)||t.pathname,s==="path");if(r==null&&(u.search=t.search,u.hash=t.hash),(r==null||r===""||r===".")&&c){let d=wN(u.search);if(c.route.index&&!d)u.search=u.search?u.search.replace(/^\?/,"?index&"):"?index";else if(!c.route.index&&d){let h=new URLSearchParams(u.search),T=h.getAll("index");h.delete("index"),T.filter(R=>R).forEach(R=>h.append("index",R));let p=h.toString();u.search=p?`?${p}`:""}}return n!=="/"&&(u.pathname=RW({basename:n,pathname:u.pathname})),pa(u)}function Wv(t,e,n){if(!n||!MW(n))return{path:e};if(n.formMethod&&!XW(n.formMethod))return{path:e,error:Ei(405,{method:n.formMethod})};let r=()=>({path:e,error:Ei(400,{type:"invalid-body"})}),s=(n.formMethod||"get").toUpperCase(),a=kP(e);if(n.body!==void 0){if(n.formEncType==="text/plain"){if(!Cr(s))return r();let T=typeof n.body=="string"?n.body:n.body instanceof FormData||n.body instanceof URLSearchParams?Array.from(n.body.entries()).reduce((p,[R,g])=>`${p}${R}=${g} +`,""):String(n.body);return{path:e,submission:{formMethod:s,formAction:a,formEncType:n.formEncType,formData:void 0,json:void 0,text:T}}}else if(n.formEncType==="application/json"){if(!Cr(s))return r();try{let T=typeof n.body=="string"?JSON.parse(n.body):n.body;return{path:e,submission:{formMethod:s,formAction:a,formEncType:n.formEncType,formData:void 0,json:T,text:void 0}}}catch{return r()}}}At(typeof FormData=="function","FormData is not available in this environment");let c,u;if(n.formData)c=kg(n.formData),u=n.formData;else if(n.body instanceof FormData)c=kg(n.body),u=n.body;else if(n.body instanceof URLSearchParams)c=n.body,u=Jv(c);else if(n.body==null)c=new URLSearchParams,u=new FormData;else try{c=new URLSearchParams(n.body),u=Jv(c)}catch{return r()}let d={formMethod:s,formAction:a,formEncType:n&&n.formEncType||"application/x-www-form-urlencoded",formData:u,json:void 0,text:void 0};if(Cr(d.formMethod))return{path:e,submission:d};let h=La(e);return t&&h.search&&wN(h.search)&&c.append("index",""),h.search=`?${c}`,{path:pa(h),submission:d}}function $v(t,e,n,r,i,s,a,c,u,d,h,T,p,R,g,N,I,C,L,y){let v=y?$r(y[1])?y[1].error:y[1].data:void 0,b=i.createURL(s.location),D=i.createURL(u),M;if(h&&s.errors){let ie=Object.keys(s.errors)[0];M=a.findIndex(se=>se.route.id===ie)}else if(y&&$r(y[1])){let ie=y[0];M=a.findIndex(se=>se.route.id===ie)-1}let w=y?y[1].statusCode:void 0,F=w&&w>=400,Q={currentUrl:b,currentParams:s.matches[0]?.params||{},nextUrl:D,nextParams:a[0].params,...c,actionResult:v,actionStatus:w},ne=a.map((ie,se)=>{let{route:Ee}=ie,U=null;if(M!=null&&se>M?U=!1:Ee.lazy?U=!0:xN(Ee)?h?U=Hg(Ee,s.loaderData,s.errors):PW(s.loaderData,s.matches[se],ie)&&(U=!0):U=!1,U!==null)return Fg(n,r,t,ie,d,e,U);let Z=F?!1:T||b.pathname+b.search===D.pathname+D.search||b.search!==D.search||xW(s.matches[se],ie),V={...Q,defaultShouldRevalidate:Z},oe=bT(ie,V);return Fg(n,r,t,ie,d,e,oe,V)}),z=[];return g.forEach((ie,se)=>{if(h||!a.some(X=>X.route.id===ie.routeId)||R.has(se))return;let Ee=s.fetchers.get(se),U=Ee&&Ee.state!=="idle"&&Ee.data===void 0,Z=na(I,ie.path,C);if(!Z){if(L&&U)return;z.push({key:se,routeId:ie.routeId,path:ie.path,matches:null,match:null,request:null,controller:null});return}if(N.has(se))return;let V=sT(Z,ie.path),oe=new AbortController,ge=vc(i,ie.path,oe.signal),x=null;if(p.has(se))p.delete(se),x=kc(n,r,ge,Z,V,d,e);else if(U)T&&(x=kc(n,r,ge,Z,V,d,e));else{let X={...Q,defaultShouldRevalidate:F?!1:T};bT(V,X)&&(x=kc(n,r,ge,Z,V,d,e,X))}x&&z.push({key:se,routeId:ie.routeId,path:ie.path,matches:x,match:V,request:ge,controller:oe})}),{dsMatches:ne,revalidatingFetchers:z}}function xN(t){return t.loader!=null||t.middleware!=null&&t.middleware.length>0}function Hg(t,e,n){if(t.lazy)return!0;if(!xN(t))return!1;let r=e!=null&&t.id in e,i=n!=null&&n[t.id]!==void 0;return!r&&i?!1:typeof t.loader=="function"&&t.loader.hydrate===!0?!0:!r&&!i}function PW(t,e,n){let r=!e||n.route.id!==e.route.id,i=!t.hasOwnProperty(n.route.id);return r||i}function xW(t,e){let n=t.route.path;return t.pathname!==e.pathname||n!=null&&n.endsWith("*")&&t.params["*"]!==e.params["*"]}function bT(t,e){if(t.route.shouldRevalidate){let n=t.route.shouldRevalidate(e);if(typeof n=="boolean")return n}return e.defaultShouldRevalidate}function zv(t,e,n,r,i,s){let a;if(t){let d=r[t];At(d,`No route found to patch children into: routeId = ${t}`),d.children||(d.children=[]),a=d.children}else a=n;let c=[],u=[];if(e.forEach(d=>{let h=a.find(T=>UP(d,T));h?u.push({existingRoute:h,newRoute:d}):c.push(d)}),c.length>0){let d=td(c,i,[t||"_","patch",String(a?.length||"0")],r);a.push(...d)}if(s&&u.length>0)for(let d=0;de.children?.some(i=>UP(n,i))):!1}var jv=new WeakMap,BP=({key:t,route:e,manifest:n,mapRouteProperties:r})=>{let i=n[e.id];if(At(i,"No route found in manifest"),!i.lazy||typeof i.lazy!="object")return;let s=i.lazy[t];if(!s)return;let a=jv.get(i);a||(a={},jv.set(i,a));let c=a[t];if(c)return c;let u=(async()=>{let d=eW(t),T=i[t]!==void 0&&t!=="hasErrorBoundary";if(d)Sn(!d,"Route property "+t+" is not a supported lazy route property. This property will be ignored."),a[t]=Promise.resolve();else if(T)Sn(!1,`Route "${i.id}" has a static property "${t}" defined. The lazy property will be ignored.`);else{let p=await s();p!=null&&(Object.assign(i,{[t]:p}),Object.assign(i,r(i)))}typeof i.lazy=="object"&&(i.lazy[t]=void 0,Object.values(i.lazy).every(p=>p===void 0)&&(i.lazy=void 0))})();return a[t]=u,u},Xv=new WeakMap;function wW(t,e,n,r,i){let s=n[t.id];if(At(s,"No route found in manifest"),!t.lazy)return{lazyRoutePromise:void 0,lazyHandlerPromise:void 0};if(typeof t.lazy=="function"){let h=Xv.get(s);if(h)return{lazyRoutePromise:h,lazyHandlerPromise:h};let T=(async()=>{At(typeof t.lazy=="function","No lazy route function found");let p=await t.lazy(),R={};for(let g in p){let N=p[g];if(N===void 0)continue;let I=nW(g),L=s[g]!==void 0&&g!=="hasErrorBoundary";I?Sn(!I,"Route property "+g+" is not a supported property to be returned from a lazy route function. This property will be ignored."):L?Sn(!L,`Route "${s.id}" has a static property "${g}" defined but its lazy function is also returning a value for this property. The lazy route property "${g}" will be ignored.`):R[g]=N}Object.assign(s,R),Object.assign(s,{...r(s),lazy:void 0})})();return Xv.set(s,T),T.catch(()=>{}),{lazyRoutePromise:T,lazyHandlerPromise:T}}let a=Object.keys(t.lazy),c=[],u;for(let h of a){if(i&&i.includes(h))continue;let T=BP({key:h,route:t,manifest:n,mapRouteProperties:r});T&&(c.push(T),h===e&&(u=T))}let d=c.length>0?Promise.all(c).then(()=>{}):void 0;return d?.catch(()=>{}),u?.catch(()=>{}),{lazyRoutePromise:d,lazyHandlerPromise:u}}async function Kv(t){let e=t.matches.filter(i=>i.shouldLoad),n={};return(await Promise.all(e.map(i=>i.resolve()))).forEach((i,s)=>{n[e[s].route.id]=i}),n}async function UW(t){return t.matches.some(e=>e.route.middleware)?GP(t,()=>Kv(t)):Kv(t)}function GP(t,e){return BW(t,e,r=>r,$W,n);function n(r,i,s){if(s)return Promise.resolve(Object.assign(s.value,{[i]:{type:"error",result:r}}));{let{matches:a}=t,c=Math.min(Math.max(a.findIndex(d=>d.route.id===i),0),Math.max(a.findIndex(d=>d.unstable_shouldCallHandler()),0)),u=ra(a,a[c].route.id).route.id;return Promise.resolve({[u]:{type:"error",result:r}})}}}async function BW(t,e,n,r,i){let{matches:s,request:a,params:c,context:u}=t,d=s.flatMap(T=>T.route.middleware?T.route.middleware.map(p=>[T.route.id,p]):[]);return await HP({request:a,params:c,context:u},d,e,n,r,i)}async function HP(t,e,n,r,i,s,a=0){let{request:c}=t;if(c.signal.aborted)throw c.signal.reason??new Error(`Request aborted: ${c.method} ${c.url}`);let u=e[a];if(!u)return await n();let[d,h]=u,T,p=async()=>{if(T)throw new Error("You may only call `next()` once per middleware");try{return T={value:await HP(t,e,n,r,i,s,a+1)},T.value}catch(R){return T={value:await s(R,d,T)},T.value}};try{let R=await h(t,p),g=R!=null?r(R):void 0;return i(g)?g:T?g??T.value:(T={value:await p()},T.value)}catch(R){return await s(R,d,T)}}function FP(t,e,n,r,i){let s=BP({key:"middleware",route:r.route,manifest:e,mapRouteProperties:t}),a=wW(r.route,Cr(n.method)?"action":"loader",e,t,i);return{middleware:s,route:a.lazyRoutePromise,handler:a.lazyHandlerPromise}}function Fg(t,e,n,r,i,s,a,c=null){let u=!1,d=FP(t,e,n,r,i);return{...r,_lazyPromises:d,shouldLoad:a,unstable_shouldRevalidateArgs:c,unstable_shouldCallHandler(h){return u=!0,c?typeof h=="boolean"?bT(r,{...c,defaultShouldRevalidate:h}):bT(r,c):a},resolve(h){let{lazy:T,loader:p,middleware:R}=r.route,g=u||a||h&&!Cr(n.method)&&(T||p),N=R&&R.length>0&&!p&&!T;return g&&!N?HW({request:n,match:r,lazyHandlerPromise:d?.handler,lazyRoutePromise:d?.route,handlerOverride:h,scopedContext:s}):Promise.resolve({type:"data",result:void 0})}}}function kc(t,e,n,r,i,s,a,c=null){return r.map(u=>u.route.id!==i.route.id?{...u,shouldLoad:!1,unstable_shouldRevalidateArgs:c,unstable_shouldCallHandler:()=>!1,_lazyPromises:FP(t,e,n,u,s),resolve:()=>Promise.resolve({type:"data",result:void 0})}:Fg(t,e,n,u,s,a,!0,c))}async function GW(t,e,n,r,i,s){n.some(d=>d._lazyPromises?.middleware)&&await Promise.all(n.map(d=>d._lazyPromises?.middleware));let a={request:e,params:n[0].params,context:i,matches:n},u=await t({...a,fetcherKey:r,runClientMiddleware:d=>{let h=a;return GP(h,()=>d({...h,fetcherKey:r,runClientMiddleware:()=>{throw new Error("Cannot call `runClientMiddleware()` from within an `runClientMiddleware` handler")}}))}});try{await Promise.all(n.flatMap(d=>[d._lazyPromises?.handler,d._lazyPromises?.route]))}catch{}return u}async function HW({request:t,match:e,lazyHandlerPromise:n,lazyRoutePromise:r,handlerOverride:i,scopedContext:s}){let a,c,u=Cr(t.method),d=u?"action":"loader",h=T=>{let p,R=new Promise((I,C)=>p=C);c=()=>p(),t.signal.addEventListener("abort",c);let g=I=>typeof T!="function"?Promise.reject(new Error(`You cannot call the handler for a route which defines a boolean "${d}" [routeId: ${e.route.id}]`)):T({request:t,params:e.params,context:s},...I!==void 0?[I]:[]),N=(async()=>{try{return{type:"data",result:await(i?i(C=>g(C)):g())}}catch(I){return{type:"error",result:I}}})();return Promise.race([N,R])};try{let T=u?e.route.action:e.route.loader;if(n||r)if(T){let p,[R]=await Promise.all([h(T).catch(g=>{p=g}),n,r]);if(p!==void 0)throw p;a=R}else{await n;let p=u?e.route.action:e.route.loader;if(p)[a]=await Promise.all([h(p),r]);else if(d==="action"){let R=new URL(t.url),g=R.pathname+R.search;throw Ei(405,{method:t.method,pathname:g,routeId:e.route.id})}else return{type:"data",result:void 0}}else if(T)a=await h(T);else{let p=new URL(t.url),R=p.pathname+p.search;throw Ei(404,{pathname:R})}}catch(T){return{type:"error",result:T}}finally{c&&t.signal.removeEventListener("abort",c)}return a}async function FW(t){let e=t.headers.get("Content-Type");return e&&/\bapplication\/json\b/.test(e)?t.body==null?null:t.json():t.text()}async function kW(t){let{result:e,type:n}=t;if(VP(e)){let r;try{r=await FW(e)}catch(i){return{type:"error",error:i}}return n==="error"?{type:"error",error:new DT(e.status,e.statusText,r),statusCode:e.status,headers:e.headers}:{type:"data",data:r,statusCode:e.status,headers:e.headers}}return n==="error"?tD(e)?e.data instanceof Error?{type:"error",error:e.data,statusCode:e.init?.status,headers:e.init?.headers?new Headers(e.init.headers):void 0}:{type:"error",error:new DT(e.init?.status||500,void 0,e.data),statusCode:nd(e)?e.status:void 0,headers:e.init?.headers?new Headers(e.init.headers):void 0}:{type:"error",error:e,statusCode:nd(e)?e.status:void 0}:tD(e)?{type:"data",data:e.data,statusCode:e.init?.status,headers:e.init?.headers?new Headers(e.init.headers):void 0}:{type:"data",data:e}}function VW(t,e,n,r,i){let s=t.headers.get("Location");if(At(s,"Redirects returned/thrown from loaders/actions must have a Location header"),!PN(s)){let a=r.slice(0,r.findIndex(c=>c.route.id===n)+1);s=Gg(new URL(e.url),a,i,s),t.headers.set("Location",s)}return t}function qv(t,e,n){if(PN(t)){let r=t,i=r.startsWith("//")?new URL(e.protocol+r):new URL(r),s=br(i.pathname,n)!=null;if(i.origin===e.origin&&s)return i.pathname+i.search+i.hash}return t}function vc(t,e,n,r){let i=t.createURL(kP(e)).toString(),s={signal:n};if(r&&Cr(r.formMethod)){let{formMethod:a,formEncType:c}=r;s.method=a.toUpperCase(),c==="application/json"?(s.headers=new Headers({"Content-Type":c}),s.body=JSON.stringify(r.json)):c==="text/plain"?s.body=r.text:c==="application/x-www-form-urlencoded"&&r.formData?s.body=kg(r.formData):s.body=r.formData}return new Request(i,s)}function kg(t){let e=new URLSearchParams;for(let[n,r]of t.entries())e.append(n,typeof r=="string"?r:r.name);return e}function Jv(t){let e=new FormData;for(let[n,r]of t.entries())e.append(n,r);return e}function YW(t,e,n,r=!1,i=!1){let s={},a=null,c,u=!1,d={},h=n&&$r(n[1])?n[1].error:void 0;return t.forEach(T=>{if(!(T.route.id in e))return;let p=T.route.id,R=e[p];if(At(!ll(R),"Cannot handle redirect results in processLoaderData"),$r(R)){let g=R.error;if(h!==void 0&&(g=h,h=void 0),a=a||{},i)a[p]=g;else{let N=ra(t,p);a[N.route.id]==null&&(a[N.route.id]=g)}r||(s[p]=wP),u||(u=!0,c=nd(R.error)?R.error.status:500),R.headers&&(d[p]=R.headers)}else s[p]=R.data,R.statusCode&&R.statusCode!==200&&!u&&(c=R.statusCode),R.headers&&(d[p]=R.headers)}),h!==void 0&&n&&(a={[n[0]]:h},n[2]&&(s[n[2]]=void 0)),{loaderData:s,errors:a,statusCode:c||200,loaderHeaders:d}}function Qv(t,e,n,r,i,s){let{loaderData:a,errors:c}=YW(e,n,r);return i.filter(u=>!u.matches||u.matches.some(d=>d.shouldLoad)).forEach(u=>{let{key:d,match:h,controller:T}=u;if(T&&T.signal.aborted)return;let p=s[d];if(At(p,"Did not find corresponding fetcher result"),$r(p)){let R=ra(t.matches,h?.route.id);c&&c[R.route.id]||(c={...c,[R.route.id]:p.error}),t.fetchers.delete(d)}else if(ll(p))At(!1,"Unhandled fetcher revalidation redirect");else{let R=io(p.data);t.fetchers.set(d,R)}}),{loaderData:a,errors:c}}function Zv(t,e,n,r){let i=Object.entries(e).filter(([,s])=>s!==wP).reduce((s,[a,c])=>(s[a]=c,s),{});for(let s of n){let a=s.route.id;if(!e.hasOwnProperty(a)&&t.hasOwnProperty(a)&&s.route.loader&&(i[a]=t[a]),r&&r.hasOwnProperty(a))break}return i}function eD(t){return t?$r(t[1])?{actionData:{}}:{actionData:{[t[0]]:t[1].data}}:{}}function ra(t,e){return(e?t.slice(0,t.findIndex(r=>r.route.id===e)+1):[...t]).reverse().find(r=>r.route.hasErrorBoundary===!0)||t[0]}function hE(t){let e=t.length===1?t[0]:t.find(n=>n.index||!n.path||n.path==="/")||{id:"__shim-error-route__"};return{matches:[{params:{},pathname:"",pathnameBase:"",route:e}],route:e}}function Ei(t,{pathname:e,routeId:n,method:r,type:i,message:s}={}){let a="Unknown Server Error",c="Unknown @remix-run/router error";return t===400?(a="Bad Request",r&&e&&n?c=`You made a ${r} request to "${e}" but did not provide a \`loader\` for route "${n}", so there is no way to handle the request.`:i==="invalid-body"&&(c="Unable to encode submission body")):t===403?(a="Forbidden",c=`Route "${n}" does not match URL "${e}"`):t===404?(a="Not Found",c=`No route matches URL "${e}"`):t===405&&(a="Method Not Allowed",r&&e&&n?c=`You made a ${r.toUpperCase()} request to "${e}" but did not provide an \`action\` for route "${n}", so there is no way to handle the request.`:r&&(c=`Invalid request method "${r.toUpperCase()}"`)),new DT(t||500,a,new Error(c),!0)}function EE(t){let e=Object.entries(t);for(let n=e.length-1;n>=0;n--){let[r,i]=e[n];if(ll(i))return{key:r,result:i}}}function kP(t){let e=typeof t=="string"?La(t):t;return pa({...e,hash:""})}function WW(t,e){return t.pathname!==e.pathname||t.search!==e.search?!1:t.hash===""?e.hash!=="":t.hash===e.hash?!0:e.hash!==""}function $W(t){return t!=null&&typeof t=="object"&&Object.entries(t).every(([e,n])=>typeof e=="string"&&zW(n))}function zW(t){return t!=null&&typeof t=="object"&&"type"in t&&"result"in t&&(t.type==="data"||t.type==="error")}function jW(t){return VP(t.result)&&_W.has(t.result.status)}function $r(t){return t.type==="error"}function ll(t){return(t&&t.type)==="redirect"}function tD(t){return typeof t=="object"&&t!=null&&"type"in t&&"data"in t&&"init"in t&&t.type==="DataWithResponseInit"}function VP(t){return t!=null&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.headers=="object"&&typeof t.body<"u"}function XW(t){return CW.has(t.toUpperCase())}function Cr(t){return NW.has(t.toUpperCase())}function wN(t){return new URLSearchParams(t).getAll("index").some(e=>e==="")}function sT(t,e){let n=typeof e=="string"?La(e).search:e.search;if(t[t.length-1].route.index&&wN(n||""))return t[t.length-1];let r=MP(t);return r[r.length-1]}function nD(t){let{formMethod:e,formAction:n,formEncType:r,text:i,formData:s,json:a}=t;if(!(!e||!n||!r)){if(i!=null)return{formMethod:e,formAction:n,formEncType:r,formData:void 0,json:void 0,text:i};if(s!=null)return{formMethod:e,formAction:n,formEncType:r,formData:s,json:void 0,text:void 0};if(a!==void 0)return{formMethod:e,formAction:n,formEncType:r,formData:void 0,json:a,text:void 0}}}function Xm(t,e){return e?{state:"loading",location:t,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text}:{state:"loading",location:t,formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0}}function KW(t,e){return{state:"submitting",location:t,formMethod:e.formMethod,formAction:e.formAction,formEncType:e.formEncType,formData:e.formData,json:e.json,text:e.text}}function Of(t,e){return t?{state:"loading",formMethod:t.formMethod,formAction:t.formAction,formEncType:t.formEncType,formData:t.formData,json:t.json,text:t.text,data:e}:{state:"loading",formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:e}}function qW(t,e){return{state:"submitting",formMethod:t.formMethod,formAction:t.formAction,formEncType:t.formEncType,formData:t.formData,json:t.json,text:t.text,data:e?e.data:void 0}}function io(t){return{state:"idle",formMethod:void 0,formAction:void 0,formEncType:void 0,formData:void 0,json:void 0,text:void 0,data:t}}function JW(t,e){try{let n=t.sessionStorage.getItem(xP);if(n){let r=JSON.parse(n);for(let[i,s]of Object.entries(r||{}))s&&Array.isArray(s)&&e.set(i,new Set(s||[]))}}catch{}}function QW(t,e){if(e.size>0){let n={};for(let[r,i]of e)n[r]=[...i];try{t.sessionStorage.setItem(xP,JSON.stringify(n))}catch(r){Sn(!1,`Failed to save applied view transitions in sessionStorage (${r}).`)}}}function ZW(){let t,e,n=new Promise((r,i)=>{t=async s=>{r(s);try{await n}catch{}},e=async s=>{i(s);try{await n}catch{}}});return{promise:n,resolve:t,reject:e}}var Dl=O.createContext(null);Dl.displayName="DataRouter";var vd=O.createContext(null);vd.displayName="DataRouterState";O.createContext(!1);var UN=O.createContext({isTransitioning:!1});UN.displayName="ViewTransition";var YP=O.createContext(new Map);YP.displayName="Fetchers";var e$=O.createContext(null);e$.displayName="Await";var ys=O.createContext(null);ys.displayName="Navigation";var Mp=O.createContext(null);Mp.displayName="Location";var $i=O.createContext({outlet:null,matches:[],isDataRoute:!1});$i.displayName="Route";var BN=O.createContext(null);BN.displayName="RouteError";function t$(t,{relative:e}={}){At(Dd(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=O.useContext(ys),{hash:i,pathname:s,search:a}=bd(t,{relative:e}),c=s;return n!=="/"&&(c=s==="/"?n:ps([n,s])),r.createHref({pathname:c,search:a,hash:i})}function Dd(){return O.useContext(Mp)!=null}function Oo(){return At(Dd(),"useLocation() may be used only in the context of a component."),O.useContext(Mp).location}var WP="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function $P(t){O.useContext(ys).static||O.useLayoutEffect(t)}function Pp(){let{isDataRoute:t}=O.useContext($i);return t?p$():n$()}function n$(){At(Dd(),"useNavigate() may be used only in the context of a component.");let t=O.useContext(Dl),{basename:e,navigator:n}=O.useContext(ys),{matches:r}=O.useContext($i),{pathname:i}=Oo(),s=JSON.stringify(bN(r)),a=O.useRef(!1);return $P(()=>{a.current=!0}),O.useCallback((u,d={})=>{if(Sn(a.current,WP),!a.current)return;if(typeof u=="number"){n.go(u);return}let h=MN(u,JSON.parse(s),i,d.relative==="path");t==null&&e!=="/"&&(h.pathname=h.pathname==="/"?e:ps([e,h.pathname])),(d.replace?n.replace:n.push)(h,d.state,d)},[e,n,s,i,t])}var r$=O.createContext(null);function i$(t){let e=O.useContext($i).outlet;return O.useMemo(()=>e&&O.createElement(r$.Provider,{value:t},e),[e,t])}function No(){let{matches:t}=O.useContext($i),e=t[t.length-1];return e?e.params:{}}function bd(t,{relative:e}={}){let{matches:n}=O.useContext($i),{pathname:r}=Oo(),i=JSON.stringify(bN(n));return O.useMemo(()=>MN(t,JSON.parse(i),r,e==="path"),[t,i,r,e])}function s$(t,e,n,r,i){At(Dd(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=O.useContext(ys),{matches:a}=O.useContext($i),c=a[a.length-1],u=c?c.params:{},d=c?c.pathname:"/",h=c?c.pathnameBase:"/",T=c&&c.route;{let L=T&&T.path||"";XP(d,!T||L.endsWith("*")||L.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${d}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let p=Oo(),R;R=p;let g=R.pathname||"/",N=g;if(h!=="/"){let L=h.replace(/^\//,"").split("/");N="/"+g.replace(/^\//,"").split("/").slice(L.length).join("/")}let I=na(t,{pathname:N});return Sn(T||I!=null,`No routes matched location "${R.pathname}${R.search}${R.hash}" `),Sn(I==null||I[I.length-1].route.element!==void 0||I[I.length-1].route.Component!==void 0||I[I.length-1].route.lazy!==void 0,`Matched leaf route at location "${R.pathname}${R.search}${R.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`),u$(I&&I.map(L=>Object.assign({},L,{params:Object.assign({},u,L.params),pathname:ps([h,s.encodeLocation?s.encodeLocation(L.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathname]),pathnameBase:L.pathnameBase==="/"?h:ps([h,s.encodeLocation?s.encodeLocation(L.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:L.pathnameBase])})),a,n,r,i)}function o$(){let t=h$(),e=nd(t)?`${t.status} ${t.statusText}`:t instanceof Error?t.message:JSON.stringify(t),n=t instanceof Error?t.stack:null,r="rgba(200,200,200, 0.5)",i={padding:"0.5rem",backgroundColor:r},s={padding:"2px 4px",backgroundColor:r},a=null;return console.error("Error handled by React Router default ErrorBoundary:",t),a=O.createElement(O.Fragment,null,O.createElement("p",null,"💿 Hey developer 👋"),O.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",O.createElement("code",{style:s},"ErrorBoundary")," or"," ",O.createElement("code",{style:s},"errorElement")," prop on your route.")),O.createElement(O.Fragment,null,O.createElement("h2",null,"Unexpected Application Error!"),O.createElement("h3",{style:{fontStyle:"italic"}},e),n?O.createElement("pre",{style:i},n):null,a)}var a$=O.createElement(o$,null),l$=class extends O.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,e){return e.location!==t.location||e.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:e.error,location:e.location,revalidation:t.revalidation||e.revalidation}}componentDidCatch(t,e){this.props.unstable_onError?this.props.unstable_onError(t,e):console.error("React Router caught the following error during render",t)}render(){return this.state.error!==void 0?O.createElement($i.Provider,{value:this.props.routeContext},O.createElement(BN.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function c$({routeContext:t,match:e,children:n}){let r=O.useContext(Dl);return r&&r.static&&r.staticContext&&(e.route.errorElement||e.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=e.route.id),O.createElement($i.Provider,{value:t},n)}function u$(t,e=[],n=null,r=null,i=null){if(t==null){if(!n)return null;if(n.errors)t=n.matches;else if(e.length===0&&!n.initialized&&n.matches.length>0)t=n.matches;else return null}let s=t,a=n?.errors;if(a!=null){let d=s.findIndex(h=>h.route.id&&a?.[h.route.id]!==void 0);At(d>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(",")}`),s=s.slice(0,Math.min(s.length,d+1))}let c=!1,u=-1;if(n)for(let d=0;d=0?s=s.slice(0,u+1):s=[s[0]];break}}}return s.reduceRight((d,h,T)=>{let p,R=!1,g=null,N=null;n&&(p=a&&h.route.id?a[h.route.id]:void 0,g=h.route.errorElement||a$,c&&(u<0&&T===0?(XP("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),R=!0,N=null):u===T&&(R=!0,N=h.route.hydrateFallbackElement||null)));let I=e.concat(s.slice(0,T+1)),C=()=>{let L;return p?L=g:R?L=N:h.route.Component?L=O.createElement(h.route.Component,null):h.route.element?L=h.route.element:L=d,O.createElement(c$,{match:h,routeContext:{outlet:d,matches:I,isDataRoute:n!=null},children:L})};return n&&(h.route.ErrorBoundary||h.route.errorElement||T===0)?O.createElement(l$,{location:n.location,revalidation:n.revalidation,component:g,error:p,children:C(),routeContext:{outlet:null,matches:I,isDataRoute:!0},unstable_onError:r}):C()},null)}function GN(t){return`${t} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function zP(t){let e=O.useContext(Dl);return At(e,GN(t)),e}function jP(t){let e=O.useContext(vd);return At(e,GN(t)),e}function f$(t){let e=O.useContext($i);return At(e,GN(t)),e}function HN(t){let e=f$(t),n=e.matches[e.matches.length-1];return At(n.route.id,`${t} can only be used on routes that contain a unique "id"`),n.route.id}function d$(){return HN("useRouteId")}function h$(){let t=O.useContext(BN),e=jP("useRouteError"),n=HN("useRouteError");return t!==void 0?t:e.errors?.[n]}var E$=0;function T$(t){let{router:e,basename:n}=zP("useBlocker"),r=jP("useBlocker"),[i,s]=O.useState(""),a=O.useCallback(c=>{if(typeof t!="function")return!!t;if(n==="/")return t(c);let{currentLocation:u,nextLocation:d,historyAction:h}=c;return t({currentLocation:{...u,pathname:br(u.pathname,n)||u.pathname},nextLocation:{...d,pathname:br(d.pathname,n)||d.pathname},historyAction:h})},[n,t]);return O.useEffect(()=>{let c=String(++E$);return s(c),()=>e.deleteBlocker(c)},[e]),O.useEffect(()=>{i!==""&&e.getBlocker(i,a)},[e,i,a]),i&&r.blockers.has(i)?r.blockers.get(i):yc}function p$(){let{router:t}=zP("useNavigate"),e=HN("useNavigate"),n=O.useRef(!1);return $P(()=>{n.current=!0}),O.useCallback(async(i,s={})=>{Sn(n.current,WP),n.current&&(typeof i=="number"?t.navigate(i):await t.navigate(i,{fromRouteId:e,...s}))},[t,e])}var rD={};function XP(t,e,n){!e&&!rD[t]&&(rD[t]=!0,Sn(!1,n))}var iD={};function sD(t,e){!t&&!iD[e]&&(iD[e]=!0,console.warn(e))}function R$(t){let e={hasErrorBoundary:t.hasErrorBoundary||t.ErrorBoundary!=null||t.errorElement!=null};return t.Component&&(t.element&&Sn(!1,"You should not include both `Component` and `element` on your route - `Component` will be used."),Object.assign(e,{element:O.createElement(t.Component),Component:void 0})),t.HydrateFallback&&(t.hydrateFallbackElement&&Sn(!1,"You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - `HydrateFallback` will be used."),Object.assign(e,{hydrateFallbackElement:O.createElement(t.HydrateFallback),HydrateFallback:void 0})),t.ErrorBoundary&&(t.errorElement&&Sn(!1,"You should not include both `ErrorBoundary` and `errorElement` on your route - `ErrorBoundary` will be used."),Object.assign(e,{errorElement:O.createElement(t.ErrorBoundary),ErrorBoundary:void 0})),e}var S$=["HydrateFallback","hydrateFallbackElement"],m$=class{constructor(){this.status="pending",this.promise=new Promise((t,e)=>{this.resolve=n=>{this.status==="pending"&&(this.status="resolved",t(n))},this.reject=n=>{this.status==="pending"&&(this.status="rejected",e(n))}})}};function A$({router:t,flushSync:e,unstable_onError:n}){let[r,i]=O.useState(t.state),[s,a]=O.useState(),[c,u]=O.useState({isTransitioning:!1}),[d,h]=O.useState(),[T,p]=O.useState(),[R,g]=O.useState(),N=O.useRef(new Map),I=O.useCallback(b=>{i(D=>(b.errors&&n&&Object.entries(b.errors).forEach(([M,w])=>{D.errors?.[M]!==w&&n(w)}),b))},[n]),C=O.useCallback((b,{deletedFetchers:D,flushSync:M,viewTransitionOpts:w})=>{b.fetchers.forEach((Q,ne)=>{Q.data!==void 0&&N.current.set(ne,Q.data)}),D.forEach(Q=>N.current.delete(Q)),sD(M===!1||e!=null,'You provided the `flushSync` option to a router update, but you are not using the `` from `react-router/dom` so `ReactDOM.flushSync()` is unavailable. Please update your app to `import { RouterProvider } from "react-router/dom"` and ensure you have `react-dom` installed as a dependency to use the `flushSync` option.');let F=t.window!=null&&t.window.document!=null&&typeof t.window.document.startViewTransition=="function";if(sD(w==null||F,"You provided the `viewTransition` option to a router update, but you do not appear to be running in a DOM environment as `window.startViewTransition` is not available."),!w||!F){e&&M?e(()=>I(b)):O.startTransition(()=>I(b));return}if(e&&M){e(()=>{T&&(d&&d.resolve(),T.skipTransition()),u({isTransitioning:!0,flushSync:!0,currentLocation:w.currentLocation,nextLocation:w.nextLocation})});let Q=t.window.document.startViewTransition(()=>{e(()=>I(b))});Q.finished.finally(()=>{e(()=>{h(void 0),p(void 0),a(void 0),u({isTransitioning:!1})})}),e(()=>p(Q));return}T?(d&&d.resolve(),T.skipTransition(),g({state:b,currentLocation:w.currentLocation,nextLocation:w.nextLocation})):(a(b),u({isTransitioning:!0,flushSync:!1,currentLocation:w.currentLocation,nextLocation:w.nextLocation}))},[t.window,e,T,d,I]);O.useLayoutEffect(()=>t.subscribe(C),[t,C]),O.useEffect(()=>{c.isTransitioning&&!c.flushSync&&h(new m$)},[c]),O.useEffect(()=>{if(d&&s&&t.window){let b=s,D=d.promise,M=t.window.document.startViewTransition(async()=>{O.startTransition(()=>I(b)),await D});M.finished.finally(()=>{h(void 0),p(void 0),a(void 0),u({isTransitioning:!1})}),p(M)}},[s,d,t.window,I]),O.useEffect(()=>{d&&s&&r.location.key===s.location.key&&d.resolve()},[d,T,r.location,s]),O.useEffect(()=>{!c.isTransitioning&&R&&(a(R.state),u({isTransitioning:!0,flushSync:!1,currentLocation:R.currentLocation,nextLocation:R.nextLocation}),g(void 0))},[c.isTransitioning,R]);let L=O.useMemo(()=>({createHref:t.createHref,encodeLocation:t.encodeLocation,go:b=>t.navigate(b),push:(b,D,M)=>t.navigate(b,{state:D,preventScrollReset:M?.preventScrollReset}),replace:(b,D,M)=>t.navigate(b,{replace:!0,state:D,preventScrollReset:M?.preventScrollReset})}),[t]),y=t.basename||"/",v=O.useMemo(()=>({router:t,navigator:L,static:!1,basename:y,unstable_onError:n}),[t,L,y,n]);return O.createElement(O.Fragment,null,O.createElement(Dl.Provider,{value:v},O.createElement(vd.Provider,{value:r},O.createElement(YP.Provider,{value:N.current},O.createElement(UN.Provider,{value:c},O.createElement(N$,{basename:y,location:r.location,navigationType:r.historyAction,navigator:L},O.createElement(g$,{routes:t.routes,future:t.future,state:r,unstable_onError:n})))))),null)}var g$=O.memo(O$);function O$({routes:t,future:e,state:n,unstable_onError:r}){return s$(t,void 0,n,r,e)}function KP(t){return i$(t.context)}function N$({basename:t="/",children:e=null,location:n,navigationType:r="POP",navigator:i,static:s=!1}){At(!Dd(),"You cannot render a inside another . You should never have more than one in your app.");let a=t.replace(/^\/*/,"/"),c=O.useMemo(()=>({basename:a,navigator:i,static:s,future:{}}),[a,i,s]);typeof n=="string"&&(n=La(n));let{pathname:u="/",search:d="",hash:h="",state:T=null,key:p="default"}=n,R=O.useMemo(()=>{let g=br(u,a);return g==null?null:{location:{pathname:g,search:d,hash:h,state:T,key:p},navigationType:r}},[a,u,d,h,T,p,r]);return Sn(R!=null,` is not able to match the URL "${u}${d}${h}" because it does not start with the basename, so the won't render anything.`),R==null?null:O.createElement(ys.Provider,{value:c},O.createElement(Mp.Provider,{children:e,value:R}))}var oT="get",aT="application/x-www-form-urlencoded";function xp(t){return t!=null&&typeof t.tagName=="string"}function I$(t){return xp(t)&&t.tagName.toLowerCase()==="button"}function C$(t){return xp(t)&&t.tagName.toLowerCase()==="form"}function _$(t){return xp(t)&&t.tagName.toLowerCase()==="input"}function L$(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function y$(t,e){return t.button===0&&(!e||e==="_self")&&!L$(t)}function Vg(t=""){return new URLSearchParams(typeof t=="string"||Array.isArray(t)||t instanceof URLSearchParams?t:Object.keys(t).reduce((e,n)=>{let r=t[n];return e.concat(Array.isArray(r)?r.map(i=>[n,i]):[[n,r]])},[]))}function v$(t,e){let n=Vg(t);return e&&e.forEach((r,i)=>{n.has(i)||e.getAll(i).forEach(s=>{n.append(i,s)})}),n}var TE=null;function D$(){if(TE===null)try{new FormData(document.createElement("form"),0),TE=!1}catch{TE=!0}return TE}var b$=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Km(t){return t!=null&&!b$.has(t)?(Sn(!1,`"${t}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${aT}"`),null):t}function M$(t,e){let n,r,i,s,a;if(C$(t)){let c=t.getAttribute("action");r=c?br(c,e):null,n=t.getAttribute("method")||oT,i=Km(t.getAttribute("enctype"))||aT,s=new FormData(t)}else if(I$(t)||_$(t)&&(t.type==="submit"||t.type==="image")){let c=t.form;if(c==null)throw new Error('Cannot submit a + + + + Show tables + + + Show views + + + + +
+ + + + + + Refresh tables + + + + + + + + Create new table + + +
+ + + + {loading + ? // Show skeleton items while loading + Array.from({ length: 8 }).map((_, index) => ( + + + + + + + + )) + : filteredTables.map(({ name, type, rowCount }) => { + const isActive = decodeURIComponent(tableName || '') === name + const Icon = type === 'view' ? ScanEye : TableIcon + return ( + + + + + {name} + + + + + + + More + + + + {type === 'table' && ( + onEditTable(name)}> + + Alter Table + + )} + {type === 'table' && ( + onTruncateTable(name, type)} + > + + Truncate Table + + )} + onDropTable(name, type)} + > + + Drop {type === 'table' ? 'Table' : 'View'} + + + + + {formatRowCount(rowCount)} + + + ) + })} + + + + + + +
+
+ Elide + Database Studio +
+ + v{VERSION} + +
+
+ + ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/DropTableDialog.tsx b/packages/cli/src/db-studio/ui/src/components/DropTableDialog.tsx new file mode 100644 index 0000000000..11a185f6a6 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/DropTableDialog.tsx @@ -0,0 +1,130 @@ +import * as React from 'react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { useDropTable } from '@/hooks/useDropTable' + +type DropTableDialogProps = { + isOpen: boolean + onOpenChange: (isOpen: boolean) => void + onSuccess?: () => void + dbId: string + tableName: string + tableType: 'table' | 'view' +} + +export function DropTableDialog({ isOpen, onOpenChange, onSuccess, dbId, tableName, tableType }: DropTableDialogProps) { + const dropTableMutation = useDropTable(dbId) + const [errorMessage, setErrorMessage] = React.useState(null) + + const handleDrop = React.useCallback(async () => { + try { + setErrorMessage(null) + await dropTableMutation.mutateAsync(tableName) + onSuccess?.() + onOpenChange(false) + } catch (error) { + const message = error instanceof Error ? error.message : 'An unknown error occurred' + setErrorMessage(message) + console.error('Drop table failed:', error) + } + }, [dropTableMutation, tableName, onOpenChange, onSuccess]) + + // Reset error when dialog opens/closes + React.useEffect(() => { + if (!isOpen) { + setErrorMessage(null) + } + }, [isOpen]) + + // Show error dialog if drop failed + if (errorMessage) { + return ( + + + + Drop Failed + + + +
+

Failed to drop this {tableType}:

+
    +
  • + + {tableName} + +
  • +
+
+
+ + {/* Show error message */} +
+
{errorMessage}
+
+ + + setErrorMessage(null)}>Close + { + e.preventDefault() + setErrorMessage(null) + handleDrop() + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Retry + + +
+
+ ) + } + + return ( + + + + Drop {tableType === 'table' ? 'Table' : 'View'} + + + +
+

+ You're about to permanently drop this {tableType} and all its data: +

+
    +
  • + + {tableName} + +
  • +
+
+
+ + + Cancel + { + e.preventDefault() + handleDrop() + }} + disabled={dropTableMutation.isPending} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {dropTableMutation.isPending ? 'Dropping...' : 'Drop'} + + +
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/HomeHeader.tsx b/packages/cli/src/db-studio/ui/src/components/HomeHeader.tsx new file mode 100644 index 0000000000..afeb98709a --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/HomeHeader.tsx @@ -0,0 +1,14 @@ +interface HomeHeaderProps { + databaseCount: number +} + +export function HomeHeader({ databaseCount }: HomeHeaderProps) { + return ( +
+ Elide +

Elide Database Studio

+

Select a database to inspect

+

{databaseCount} databases found

+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/QueryEditor.tsx b/packages/cli/src/db-studio/ui/src/components/QueryEditor.tsx new file mode 100644 index 0000000000..3e8ab7b677 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/QueryEditor.tsx @@ -0,0 +1,107 @@ +import { useMemo, useRef, useEffect } from 'react' +import { Play, X, AlignLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Kbd } from '@/components/ui/kbd' +import CodeMirror from '@uiw/react-codemirror' +import { sql as sqlLang } from '@codemirror/lang-sql' +import { oneDark } from '@codemirror/theme-one-dark' +import { keymap } from '@codemirror/view' +import { Prec } from '@codemirror/state' +import { format } from 'sql-formatter' + +interface QueryEditorProps { + sql: string + onSqlChange: (sql: string) => void + onExecute: () => void + loading: boolean +} + +export function QueryEditor({ sql, onSqlChange, onExecute, loading }: QueryEditorProps) { + const handleClear = () => onSqlChange('') + + const handleFormat = () => { + if (!sql.trim()) return + try { + const formatted = format(sql, { + language: 'sql', + tabWidth: 2, + keywordCase: 'upper', + }) + onSqlChange(formatted) + } catch (err) { + // If formatting fails, just keep the original SQL + console.error('Formatting error:', err) + } + } + + const sqlRef = useRef(sql) + const onExecuteRef = useRef(onExecute) + + useEffect(() => { + sqlRef.current = sql + onExecuteRef.current = onExecute + }, [sql, onExecute]) + + const executeKeymap = useMemo( + () => + Prec.highest( + keymap.of([ + { + key: 'Mod-Enter', + run: () => { + const currentSql = sqlRef.current.trim() + if (!currentSql) return false + onExecuteRef.current() + return true + }, + }, + ]) + ), + [] + ) + + return ( +
+
+

SQL Query Editor

+ + + +
+
+
+ onSqlChange(value)} + placeholder="Enter your SQL query here... (Cmd/Ctrl + Enter to execute)" + height="100%" + extensions={[sqlLang(), executeKeymap]} + theme={oneDark} + basicSetup={{ + lineNumbers: true, + foldGutter: false, + dropCursor: false, + allowMultipleSelections: false, + }} + editable={!loading} + className="w-full h-full [&_.cm-editor]:bg-background [&_.cm-editor]:border-0 [&_.cm-editor]:rounded-none [&_.cm-scroller]:font-mono [&_.cm-content]:text-foreground [&_.cm-content]:text-sm [&_.cm-placeholder]:text-muted-foreground [&_.cm-editor]:w-full [&_.cm-gutters]:bg-background [&_.cm-lineNumbers]:text-muted-foreground [&_.cm-editor]:p-0 [&_.cm-scroller]:p-0 [&_.cm-content]:p-0 [&_.cm-editor]:h-full" + /> +
+
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/QueryResults.tsx b/packages/cli/src/db-studio/ui/src/components/QueryResults.tsx new file mode 100644 index 0000000000..eb0c1c7c21 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/QueryResults.tsx @@ -0,0 +1,230 @@ +import { useMemo } from 'react' +import { getCoreRowModel, useReactTable } from '@tanstack/react-table' +import type { ColumnDef } from '@tanstack/react-table' +import { DataTable, DataTableProvider } from '@/components/data-table' +import type { ColumnMetadata } from '@/lib/types' + +interface SimpleColumnMetadata { + name: string + type: string | null +} + +interface QueryMetadata { + executionTimeMs: number +} + +interface SelectResult { + columns: SimpleColumnMetadata[] + data: Record[] + metadata: QueryMetadata +} + +interface MutationResult { + metadata: QueryMetadata +} + +type QueryResult = SelectResult | MutationResult + +/** + * Convert simple query result columns to full ColumnMetadata format + */ +function toFullColumnMetadata(columns: SimpleColumnMetadata[]): ColumnMetadata[] { + return columns.map((col) => ({ + name: col.name, + type: col.type ?? 'unknown', + nullable: true, + primaryKey: false, + })) +} + +interface QueryResultsProps { + result: QueryResult | null + loading: boolean + error: Error | null +} + +export function QueryResults({ result, loading, error }: QueryResultsProps) { + // Memoize data object for DataTable + const tableData = useMemo(() => { + if (!result || !('data' in result)) return null + return { + columns: toFullColumnMetadata(result.columns), + rows: result.data.map((row) => result.columns.map((col) => row[col.name])), + metadata: result.metadata, + } + }, [result]) + + if (loading) { + return
Executing query...
+ } + + if (error) { + return + } + + if (result) { + if ('data' in result) { + if (result.data.length > 0 && tableData) { + return + } + return
No rows returned
+ } + return + } + + return ( +
+ Enter a SQL query and click Execute Query to see results +
+ ) +} + +/** + * Component to display SQL errors + */ +function QueryError({ error }: { error: Error }) { + // Try to extract and display SQL from error response + const errorData = (error as any).response || {} + + return ( +
+
+

+ + + + SQL Error +

+

{error.message}

+ + {errorData.sql && ( +
+
Failed Query:
+
+              {errorData.sql}
+            
+ {errorData.executionTimeMs !== undefined && ( +
+ Execution time: {errorData.executionTimeMs}ms +
+ )} +
+ )} +
+
+ ) +} + +/** + * Component to display mutation (non-SELECT) results + */ +function MutationResultDisplay({ result }: { result: MutationResult }) { + return ( +
+
+
+
+ Status: + Success +
+
+ Execution time: + {result.metadata.executionTimeMs}ms +
+
+
+
+ ) +} + +/** + * Component to render query results in a DataTable + * Creates a simple table instance without server-side state + */ +function QueryResultsTable({ + tableData, + totalRows, +}: { + tableData: { columns: ColumnMetadata[]; rows: unknown[][]; metadata: QueryMetadata } + totalRows: number +}) { + // Full columns for context + const fullColumns = tableData.columns + // Build TanStack Table columns + const tableColumns: ColumnDef>[] = useMemo(() => { + return tableData.columns.map((col) => ({ + accessorKey: col.name, + size: 200, + minSize: 100, + maxSize: 1000, + enableResizing: true, + header: () => ( +
+ {col.name} +
+ ), + cell: ({ getValue }: { getValue: () => unknown }) => { + const value = getValue() + if (value === null || value === undefined) + return NULL + return String(value) + }, + })) + }, [tableData.columns]) + + // Build TanStack Table data + const tableRows: Record[] = useMemo(() => { + return tableData.rows.map((row) => + tableData.columns.reduce( + (acc, col, idx) => { + acc[col.name] = row[idx] + return acc + }, + {} as Record + ) + ) + }, [tableData.rows, tableData.columns]) + + // Create TanStack Table instance (no sorting, filtering, or pagination) + const table = useReactTable({ + data: tableRows, + columns: tableColumns, + getCoreRowModel: getCoreRowModel(), + enableColumnResizing: true, + columnResizeMode: 'onChange', + }) + + // Build context value + const contextValue = useMemo( + () => ({ + table, + columns: fullColumns, + rowCount: tableData.rows.length, + metadata: tableData.metadata, + pagination: { limit: totalRows, offset: 0 }, + sorting: { column: null, direction: null }, + appliedFilters: [], + onPaginationChange: () => {}, + onSortingChange: () => {}, + onFiltersChange: () => {}, + config: { + totalRows, + isLoading: false, + showControls: false, + showPagination: false, + }, + }), + [table, fullColumns, tableData.rows.length, tableData.metadata, totalRows] + ) + + return ( + + + + ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/TruncateTableDialog.tsx b/packages/cli/src/db-studio/ui/src/components/TruncateTableDialog.tsx new file mode 100644 index 0000000000..93e50249c9 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/TruncateTableDialog.tsx @@ -0,0 +1,127 @@ +import * as React from 'react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { useTruncateTable } from '@/hooks/useTruncateTable' + +type TruncateTableDialogProps = { + isOpen: boolean + onOpenChange: (isOpen: boolean) => void + dbId: string + tableName: string +} + +export function TruncateTableDialog({ isOpen, onOpenChange, dbId, tableName }: TruncateTableDialogProps) { + const truncateTableMutation = useTruncateTable(dbId) + const [errorMessage, setErrorMessage] = React.useState(null) + + const handleTruncate = React.useCallback(async () => { + try { + setErrorMessage(null) + await truncateTableMutation.mutateAsync(tableName) + onOpenChange(false) + } catch (error) { + const message = error instanceof Error ? error.message : 'An unknown error occurred' + setErrorMessage(message) + console.error('Truncate table failed:', error) + } + }, [truncateTableMutation, tableName, onOpenChange]) + + // Reset error when dialog opens/closes + React.useEffect(() => { + if (!isOpen) { + setErrorMessage(null) + } + }, [isOpen]) + + // Show error dialog if truncate failed + if (errorMessage) { + return ( + + + + Truncate Failed + + + +
+

Failed to truncate this table:

+
    +
  • + + {tableName} + +
  • +
+
+
+ + {/* Show error message */} +
+
{errorMessage}
+
+ + + setErrorMessage(null)}>Close + { + e.preventDefault() + setErrorMessage(null) + handleTruncate() + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Retry + + +
+
+ ) + } + + return ( + + + + Truncate Table + + + +
+

+ You're about to permanently delete all rows in this table: +

+
    +
  • + + {tableName} + +
  • +
+
+
+ + + Cancel + { + e.preventDefault() + handleTruncate() + }} + disabled={truncateTableMutation.isPending} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {truncateTableMutation.isPending ? 'Truncating...' : 'Truncate'} + + +
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/AddRowButton.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/AddRowButton.tsx new file mode 100644 index 0000000000..e8edc70918 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/AddRowButton.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { PlusCircle } from 'lucide-react' +import { Button } from '@/components/ui/button' + +type AddRowButtonProps = { + onClick: () => void + disabled?: boolean +} + +export const AddRowButton = React.memo(function AddRowButton({ onClick, disabled }: AddRowButtonProps) { + return ( + + ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/ColumnHeader.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/ColumnHeader.tsx new file mode 100644 index 0000000000..502bb80cd8 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/ColumnHeader.tsx @@ -0,0 +1,165 @@ +import * as React from 'react' +import { Key, Link, ArrowUpDown, ArrowUpWideNarrow, ArrowDownWideNarrow } from 'lucide-react' +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' +import type { ColumnMetadata } from '@/lib/types' +import { useDataTable } from '@/contexts/DataTableContext' + +type ColumnInfoProps = { + column: ColumnMetadata + children: React.ReactNode +} + +/** + * Hover card component that displays detailed column metadata + * Shows type, nullable status, constraints, foreign keys, etc. + */ +function ColumnInfo({ column, children }: ColumnInfoProps) { + return ( + + {children} + +
+
+

{column.name}

+
+ + {column.primaryKey && ( +
+
+ + Primary Key +
+ Yes +
+ )} + + {column.foreignKey && ( +
+
+ + Foreign Key +
+
+
+ References + + {column.foreignKey.table}.{column.foreignKey.column} + +
+ {column.foreignKey.onUpdate && ( +
+ On Update + {column.foreignKey.onUpdate} +
+ )} + {column.foreignKey.onDelete && ( +
+ On Delete + {column.foreignKey.onDelete} +
+ )} +
+
+ )} + +
+
+ Type + {column.type} +
+ +
+ Nullable + {column.nullable ? 'Yes' : 'No'} +
+ + {column.unique && ( +
+ Unique + Yes +
+ )} + + {column.autoIncrement && ( +
+ Auto Increment + Yes +
+ )} + + {column.defaultValue !== undefined && column.defaultValue !== null && ( +
+ Default + {String(column.defaultValue)} +
+ )} +
+
+
+
+ ) +} + +type SortIconProps = { + sorted: false | 'asc' | 'desc' + className: string +} + +function SortIcon({ sorted, className }: SortIconProps) { + switch (sorted) { + case 'asc': + return + case 'desc': + return + default: + return + } +} + +type ColumnHeaderProps = { + column: ColumnMetadata + sorted: false | 'asc' | 'desc' + showControls?: boolean +} + +/** + * Column header component that displays column name, type, and optional sorting controls + * Wraps content in a ColumnInfo hover card for displaying detailed metadata + */ +export function ColumnHeader({ column, sorted, showControls = true }: ColumnHeaderProps) { + const { onSortingChange } = useDataTable() + + const isKey = column.primaryKey + const isForeignKey = !!column.foreignKey + const isSortable = showControls + + const handleSort = React.useCallback(() => { + // Three-state sorting: no sort → asc → desc → no sort + if (!sorted) { + onSortingChange({ column: column.name, direction: 'asc' }) + } else if (sorted === 'asc') { + onSortingChange({ column: column.name, direction: 'desc' }) + } else { + onSortingChange({ column: null, direction: null }) + } + }, [sorted, onSortingChange, column.name]) + + return ( + +
+ {isKey && } + {isForeignKey && } +
+ {column.name} + {column.type} +
+ {isSortable && } +
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/ColumnsDropdown.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/ColumnsDropdown.tsx new file mode 100644 index 0000000000..0dc18ac944 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/ColumnsDropdown.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Settings2, Search } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { useDataTable } from '@/contexts/DataTableContext' + +type ColumnsDropdownProps = { + disabled?: boolean +} + +export const ColumnsDropdown = function ColumnsDropdown({ disabled }: ColumnsDropdownProps) { + const { table } = useDataTable() + + // Local search state + const [search, setSearch] = React.useState('') + + const hasHiddenColumns = table.getAllColumns().some((col) => col.getCanHide() && !col.getIsVisible()) + + return ( + !open && setSearch('')}> + +
+ + {hasHiddenColumns && ( + + ! + + )} +
+
+ e.preventDefault()}> + Toggle columns + +
e.stopPropagation()}> +
+ + setSearch(e.target.value)} + className="h-8 text-xs pl-8 w-full" + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + autoFocus + /> +
+
+ + +
+ { + e.preventDefault() + const hasHiddenColumns = table.getAllColumns().some((col) => !col.getIsVisible() && col.getCanHide()) + table.getAllColumns().forEach((col) => { + if (col.getCanHide()) { + col.toggleVisibility(hasHiddenColumns) + } + }) + }} + > + {table.getAllColumns().some((col) => !col.getIsVisible() && col.getCanHide()) + ? 'Select all' + : 'Deselect all'} + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .filter((column) => column.id.toLowerCase().includes(search.toLowerCase())) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + onSelect={(e) => e.preventDefault()} + > + {column.id} + + ) + })} + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .filter((column) => column.id.toLowerCase().includes(search.toLowerCase())).length === 0 && ( +
No columns found
+ )} +
+
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DataTable.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DataTable.tsx new file mode 100644 index 0000000000..37c22e3426 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DataTable.tsx @@ -0,0 +1,374 @@ +import * as React from 'react' +import { useBlocker, useParams } from 'react-router-dom' +import { useQueryClient } from '@tanstack/react-query' +import { useDataTable } from '@/contexts/DataTableContext' +import { useInsertRow } from '@/hooks/useInsertRow' +import { useUpdateRow } from '@/hooks/useUpdateRow' +import { DataTableToolbar } from './DataTableToolbar' +import { DataTableFilterPanel } from './DataTableFilterPanel' +import { DataTableGrid } from './DataTableGrid' +import { DeleteRowsDialog } from './DeleteRowsDialog' +import { InsertRowDialog, type InsertResult } from './InsertRowDialog' +import { DiscardChangesDialog } from './DiscardChangesDialog' + +/** + * Represents a pending cell edit on an existing row + */ +export type CellEdit = { + /** Unique ID for tracking this edit (rowIndex-columnName) */ + id: string + /** The row index in the table (for display purposes) */ + rowIndex: number + /** Primary key values to identify the row */ + primaryKey: Record + /** Column name being edited */ + columnName: string + /** Original value before edit */ + originalValue: unknown + /** New value after edit */ + newValue: unknown +} + +/** + * Reusable data table component for displaying database query results + * Uses TanStack Table for sorting, filtering, and column visibility + * All state management is handled by Table.tsx route component + * Must be wrapped in a DataTableProvider + */ +export type EditableRowData = { + id: string + data: Record +} + +export function DataTable() { + const { config, appliedFilters, columns, table } = useDataTable() + const insertRowMutation = useInsertRow() + const updateRowMutation = useUpdateRow() + const queryClient = useQueryClient() + const { dbId, tableName } = useParams<{ dbId: string; tableName: string }>() + + // Local state for filter panel visibility + const [showFilterPanel, setShowFilterPanel] = React.useState(appliedFilters.length > 0) + + // Local state for delete dialog visibility + const [showDeleteDialog, setShowDeleteDialog] = React.useState(false) + + // Local state for editable rows - array of rows to insert + const [editableRows, setEditableRows] = React.useState([]) + + // Local state for cell edits - pending updates to existing rows + const [cellEdits, setCellEdits] = React.useState([]) + + // Local state for dialogs + const [showInsertDialog, setShowInsertDialog] = React.useState(false) + const [insertResults, setInsertResults] = React.useState([]) + + // Block navigation when there are unsaved editable rows or cell edits + const blocker = useBlocker(editableRows.length > 0 || cellEdits.length > 0) + + // Show panel when filters are applied + React.useEffect(() => { + if (appliedFilters.length > 0) { + setShowFilterPanel(true) + } + }, [appliedFilters.length]) + + const handleFilterToggle = React.useCallback(() => { + setShowFilterPanel((prev) => !prev) + }, []) + + const handleAddFilter = React.useCallback(() => { + setShowFilterPanel(true) + }, []) + + const handleClearFilters = React.useCallback(() => { + setShowFilterPanel(false) + }, []) + + const handleDeleteRows = React.useCallback(() => { + setShowDeleteDialog(true) + }, []) + + // Add row handlers + const handleAddRow = React.useCallback(() => { + // Create a new row with all values defaulting to null + const initialRowData: Record = {} + columns.forEach((col) => { + initialRowData[col.name] = null + }) + + const newRow: EditableRowData = { + id: `new-${Date.now()}-${Math.random()}`, + data: initialRowData, + } + + // Show all columns so user can fill in all fields + table.toggleAllColumnsVisible(true) + setShowFilterPanel(false) + + setEditableRows((prev) => [...prev, newRow]) + }, [columns, table, setShowFilterPanel]) + + const handleCellChange = React.useCallback((rowId: string, columnName: string, value: unknown) => { + setEditableRows((prev) => + prev.map((row) => { + if (row.id === rowId) { + return { + ...row, + data: { + ...row.data, + [columnName]: value, + }, + } + } + return row + }) + ) + }, []) + + const handleRemoveRow = React.useCallback((rowId: string) => { + setEditableRows((prev) => prev.filter((row) => row.id !== rowId)) + }, []) + + // Get primary key columns + const pkColumns = React.useMemo(() => columns.filter((col) => col.primaryKey), [columns]) + + // Handler for committing a cell edit (on existing row) + const handleCellEditCommit = React.useCallback( + (rowIndex: number, columnName: string, newValue: unknown) => { + // Get the row data from the table + const row = table.getRowModel().rows[rowIndex] + if (!row) return + + const rowData = row.original + const originalValue = rowData[columnName] + + // If value hasn't changed, do nothing + if (originalValue === newValue) return + + // Build primary key + const primaryKey: Record = {} + for (const pkCol of pkColumns) { + primaryKey[pkCol.name] = rowData[pkCol.name] + } + + const editId = `${rowIndex}-${columnName}` + + setCellEdits((prev) => { + // Check if there's already an edit for this cell + const existingIndex = prev.findIndex((edit) => edit.id === editId) + + if (existingIndex >= 0) { + // Update existing edit + const existingEdit = prev[existingIndex] + // If new value equals original, remove the edit + if (existingEdit.originalValue === newValue) { + return prev.filter((_, i) => i !== existingIndex) + } + // Otherwise update the edit + return prev.map((edit, i) => (i === existingIndex ? { ...edit, newValue } : edit)) + } + + // Add new edit + return [ + ...prev, + { + id: editId, + rowIndex, + primaryKey, + columnName, + originalValue, + newValue, + }, + ] + }) + }, + [table, pkColumns] + ) + + // Handler for canceling a cell edit + const handleCellEditCancel = React.useCallback((rowIndex: number, columnName: string) => { + const editId = `${rowIndex}-${columnName}` + setCellEdits((prev) => prev.filter((edit) => edit.id !== editId)) + }, []) + + const handleSaveAll = React.useCallback(async () => { + if (editableRows.length === 0 && cellEdits.length === 0) return + + const results: InsertResult[] = [] + const successfulRowIds: string[] = [] + const successfulEditIds: string[] = [] + let hasFailures = false + + // Process cell edits (updates) first - group by row for efficiency + const editsByRow = new Map() + for (const edit of cellEdits) { + const key = JSON.stringify(edit.primaryKey) + const existing = editsByRow.get(key) || [] + editsByRow.set(key, [...existing, edit]) + } + + // Process each row's updates + for (const [, edits] of editsByRow) { + const primaryKey = edits[0].primaryKey + const updates: Record = {} + for (const edit of edits) { + updates[edit.columnName] = edit.newValue + } + + try { + const response = await updateRowMutation.mutateAsync({ primaryKey, updates }) + + // Mark all edits for this row as successful + for (const edit of edits) { + results.push({ + id: edit.id, + status: 'success' as const, + sql: response.sql, + }) + successfulEditIds.push(edit.id) + } + } catch (error: unknown) { + hasFailures = true + const message = error instanceof Error ? error.message : 'An unknown error occurred' + const sql = (error as { response?: { sql?: string } })?.response?.sql ?? undefined + + // Mark all edits for this row as failed + for (const edit of edits) { + results.push({ + id: edit.id, + status: 'error' as const, + sql, + error: message, + }) + } + } + } + + // Process new rows (inserts) + for (const row of editableRows) { + try { + const response = await insertRowMutation.mutateAsync(row.data) + + results.push({ + id: row.id, + status: 'success' as const, + sql: response.sql, + }) + successfulRowIds.push(row.id) + } catch (error: unknown) { + hasFailures = true + const message = error instanceof Error ? error.message : 'An unknown error occurred' + const sql = (error as { response?: { sql?: string } })?.response?.sql ?? undefined + + results.push({ + id: row.id, + status: 'error' as const, + sql, + error: message, + }) + } + } + + // Remove successful rows from editable rows + setEditableRows((prev) => prev.filter((row) => !successfulRowIds.includes(row.id))) + + // Remove successful cell edits + setCellEdits((prev) => prev.filter((edit) => !successfulEditIds.includes(edit.id))) + + // Refresh table data to show changes + if (successfulRowIds.length > 0 || successfulEditIds.length > 0) { + queryClient.invalidateQueries({ + queryKey: ['databases', dbId, 'tables', tableName], + }) + } + + // Only show dialog if there were failures + if (hasFailures) { + setInsertResults(results) + setShowInsertDialog(true) + } + }, [editableRows, cellEdits, insertRowMutation, updateRowMutation, queryClient, dbId, tableName]) + + // Immediate discard when user clicks the discard button (intentional action) + const handleDiscardAll = React.useCallback(() => { + setEditableRows([]) + setCellEdits([]) + }, []) + + // Handle navigation blocking - discard and proceed with navigation + const handleBlockedDiscard = React.useCallback(() => { + setEditableRows([]) + setCellEdits([]) + if (blocker.state === 'blocked') { + blocker.proceed() + } + }, [blocker]) + + const handleCloseInsertDialog = React.useCallback(() => { + setInsertResults([]) + setShowInsertDialog(false) + }, []) + + // Only allow deletion for tables, not views + const isView = config.tableType === 'view' + const deleteHandler = isView ? undefined : handleDeleteRows + const addRowHandler = isView ? undefined : handleAddRow + const hasEditableRows = editableRows.length > 0 + const hasCellEdits = cellEdits.length > 0 + const hasPendingChanges = hasEditableRows || hasCellEdits + const canEditCells = !isView && pkColumns.length > 0 + + return ( +
+ {/* Toolbar with metadata and controls */} + {(config.showControls || config.showPagination || config.tableName) && ( + + )} + + {/* Filter rows section */} + {showFilterPanel && } + + {/* Table */} + + + {/* Delete rows dialog */} + + + {/* Insert row dialog */} + + + {/* Discard changes dialog - shown when navigating away with unsaved changes */} + { + if (!open && blocker.state === 'blocked') { + blocker.reset() + } + }} + onDiscard={handleBlockedDiscard} + /> +
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DataTableFilterPanel.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DataTableFilterPanel.tsx new file mode 100644 index 0000000000..966534ace8 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DataTableFilterPanel.tsx @@ -0,0 +1,162 @@ +import * as React from 'react' +import { Plus } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { FilterRow } from './FilterRow' +import { useDataTable } from '@/contexts/DataTableContext' +import type { Filter } from '@/lib/types' + +/** + * Deep equality check for filters + */ +function areFiltersEqual(a: Filter[], b: Filter[]): boolean { + if (a.length !== b.length) return false + return a.every((filterA, index) => { + const filterB = b[index] + return ( + filterA.column === filterB.column && + filterA.operator === filterB.operator && + JSON.stringify(filterA.value) === JSON.stringify(filterB.value) + ) + }) +} + +type DataTableFilterPanelProps = { + onClearFilters: () => void +} + +export const DataTableFilterPanel = React.memo(function DataTableFilterPanel({ + onClearFilters, +}: DataTableFilterPanelProps) { + const { columns, appliedFilters, onFiltersChange } = useDataTable() + + // Local draft state (edit here, apply to URL) + const [draftFilters, setDraftFilters] = React.useState(() => { + // Always start with at least one filter + if (appliedFilters.length > 0) { + return appliedFilters + } + return [ + { + column: columns[0]?.name || '', + operator: 'eq', + value: '', + }, + ] + }) + + // Sync draft when appliedFilters change (from URL navigation) + React.useEffect(() => { + // Always maintain at least one filter + if (appliedFilters.length > 0) { + setDraftFilters(appliedFilters) + } else { + setDraftFilters([ + { + column: columns[0]?.name || '', + operator: 'eq', + value: '', + }, + ]) + } + }, [appliedFilters, columns]) + + // Check if draft differs from applied + const hasUnappliedChanges = React.useMemo(() => { + return !areFiltersEqual(draftFilters, appliedFilters) + }, [draftFilters, appliedFilters]) + + const handleAdd = React.useCallback(() => { + const newFilter: Filter = { + column: columns[0]?.name || '', + operator: 'eq', + value: '', + } + setDraftFilters((prev) => [...prev, newFilter]) + }, [columns]) + + const handleUpdate = React.useCallback((index: number, updates: Partial) => { + setDraftFilters((prev) => + prev.map((filter, i) => { + if (i === index) { + const updatedFilter = { ...filter, ...updates } + // Reset value if operator changes to one that doesn't need a value + if (updates.operator && (updates.operator === 'is_null' || updates.operator === 'is_not_null')) { + updatedFilter.value = undefined + } + // Reset value if operator changes to 'in' (needs array) + if (updates.operator === 'in' && !Array.isArray(updatedFilter.value)) { + updatedFilter.value = [] + } + // Reset value to string if operator changes from 'in' to something else + if (updates.operator && updates.operator !== 'in' && Array.isArray(updatedFilter.value)) { + updatedFilter.value = '' + } + return updatedFilter + } + return filter + }) + ) + }, []) + + const handleApply = React.useCallback(() => { + onFiltersChange(draftFilters) + }, [draftFilters, onFiltersChange]) + + const handleClear = React.useCallback(() => { + setDraftFilters([]) + onFiltersChange([]) + onClearFilters() + }, [onFiltersChange, onClearFilters]) + + const handleRemove = React.useCallback( + (index: number) => { + if (draftFilters.length === 1) { + handleClear() + } + setDraftFilters((prev) => prev.filter((_, i) => i !== index)) + }, + [handleClear, draftFilters.length] + ) + + return ( +
+
+
+ {draftFilters.map((filter, index) => ( + handleUpdate(index, updates)} + onRemove={() => handleRemove(index)} + /> + ))} +
+ {/* Separator */} +
+ {/* Action buttons column */} +
+ + + +
+
+
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DataTableGrid.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DataTableGrid.tsx new file mode 100644 index 0000000000..4f83313048 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DataTableGrid.tsx @@ -0,0 +1,344 @@ +import * as React from 'react' +import { flexRender, type Row } from '@tanstack/react-table' + +import { TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Skeleton } from '@/components/ui/skeleton' +import { useDataTable } from '@/contexts/DataTableContext' +import { EditableRow } from './EditableRow' +import { InlineEditableCell } from './InlineEditableCell' +import type { CellEdit, EditableRowData } from './DataTable' + +type SelectedCell = { rowId: string; columnId: string } | null +type EditingCell = { rowIndex: number; columnId: string } | null + +type DataTableGridProps = { + editableRows?: EditableRowData[] + onEditCellChange?: (rowId: string, columnName: string, value: unknown) => void + onEditRowRemove?: (rowId: string) => void + cellEdits?: CellEdit[] + onCellEditCommit?: (rowIndex: number, columnName: string, newValue: unknown) => void + onCellEditCancel?: (rowIndex: number, columnName: string) => void +} + +type MemoizedRowProps = { + row: Row> + rowIndex: number + isLoading: boolean + isResizing: boolean + selectedCell: SelectedCell + editingCell: EditingCell + editingValue: unknown + cellEdits: CellEdit[] + columns: { name: string; type: string; nullable: boolean; primaryKey: boolean; autoIncrement?: boolean }[] + onCellClick: (rowId: string, columnId: string) => void + onCellDoubleClick?: (rowIndex: number, columnId: string) => void + onEditChange: (value: unknown) => void + onEditCommit: () => void + onEditCancel: () => void +} + +/** + * Checks if a row should skip rerendering + * Returns true to skip rerender, false to allow rerender + */ +function shouldSkipRowRerender(prev: MemoizedRowProps, next: MemoizedRowProps): boolean { + // Rerender if loading state changed + if (prev.isLoading !== next.isLoading) return false + + // Rerender if row identity changed + if (prev.row.id !== next.row.id) return false + + // Rerender if visible columns changed (column visibility toggled) + if (prev.row.getVisibleCells().length !== next.row.getVisibleCells().length) return false + + // Rerender if selected cell in this row changed + const prevHasSelection = prev.selectedCell?.rowId === prev.row.id + const nextHasSelection = next.selectedCell?.rowId === next.row.id + if (prevHasSelection !== nextHasSelection) return false + if (prevHasSelection && nextHasSelection && prev.selectedCell?.columnId !== next.selectedCell?.columnId) return false + + // Rerender if editing state for this row changed + const prevIsEditing = prev.editingCell?.rowIndex === prev.rowIndex + const nextIsEditing = next.editingCell?.rowIndex === next.rowIndex + if (prevIsEditing !== nextIsEditing) return false + if (prevIsEditing && nextIsEditing && prev.editingCell?.columnId !== next.editingCell?.columnId) return false + // Rerender if editing value changed for this row + if (nextIsEditing && prev.editingValue !== next.editingValue) return false + + // Rerender if cell edits for this row changed + const prevEdits = prev.cellEdits.filter((e) => e.rowIndex === prev.rowIndex) + const nextEdits = next.cellEdits.filter((e) => e.rowIndex === next.rowIndex) + if (prevEdits.length !== nextEdits.length) return false + for (let i = 0; i < prevEdits.length; i++) { + if (prevEdits[i].columnName !== nextEdits[i].columnName || prevEdits[i].newValue !== nextEdits[i].newValue) + return false + } + + // Skip rerender only if we're currently resizing + return next.isResizing +} + +/** + * Memoized table row that prevents rerenders during column resizing + * Maintains smooth performance with large datasets by blocking rerenders during resize operations + */ +const MemoizedRow = React.memo( + ({ + row, + rowIndex, + isLoading, + selectedCell, + editingCell, + editingValue, + cellEdits, + columns, + onCellClick, + onCellDoubleClick, + onEditChange, + onEditCommit, + onEditCancel, + }: MemoizedRowProps) => { + const isSelected = row.getIsSelected() + + return ( + + {row.getVisibleCells().map((cell) => { + const { id, column, getContext } = cell + const width = column.getSize() + const isCheckboxColumn = column.id === 'select' + const isCellSelected = + !isCheckboxColumn && selectedCell?.rowId === row.id && selectedCell?.columnId === column.id + const isEditing = + !isCheckboxColumn && editingCell?.rowIndex === rowIndex && editingCell?.columnId === column.id + + // Check if this cell has a pending edit + const cellEdit = cellEdits.find((e) => e.rowIndex === rowIndex && e.columnName === column.id) + const hasEdit = !!cellEdit + + // Get column metadata for the editable cell + const columnMeta = columns.find((col) => col.name === column.id) + + // Get the display value: use editingValue when actively editing, otherwise edited or original + const displayValue = isEditing ? editingValue : hasEdit ? cellEdit.newValue : row.original[column.id] + + // Style cells that are selected, editing, or have edits with blue highlight + const hasBlueHighlight = isCellSelected || isEditing || hasEdit + + return ( + onCellClick(row.id, column.id)} + onDoubleClick={ + isCheckboxColumn || isEditing || !onCellDoubleClick + ? undefined + : () => onCellDoubleClick(rowIndex, column.id) + } + className={`text-xs text-foreground border-r border-border overflow-hidden truncate font-mono transition-colors ${ + isCheckboxColumn ? 'px-4 py-2' : 'cursor-pointer' + } ${!isCheckboxColumn && !isEditing ? 'px-4 py-2' : ''} ${ + hasBlueHighlight ? 'bg-blue-500/20 ring-2 ring-blue-500 ring-inset' : '' + } ${isEditing ? 'p-0' : ''}`} + style={{ width, maxWidth: width }} + > + {isLoading ? ( + + ) : isEditing && columnMeta ? ( + + ) : hasEdit ? ( + // Show edited value + displayValue === null ? ( + NULL + ) : displayValue === '' ? ( + EMPTY STRING + ) : ( + String(displayValue) + ) + ) : ( + flexRender(column.columnDef.cell, getContext()) + )} + + ) + })} + + ) + }, + shouldSkipRowRerender +) + +MemoizedRow.displayName = 'MemoizedRow' + +export function DataTableGrid({ + editableRows, + onEditCellChange, + onEditRowRemove, + cellEdits = [], + onCellEditCommit, + onCellEditCancel, +}: DataTableGridProps) { + const { table, config, pagination, columns } = useDataTable() + const [selectedCell, setSelectedCell] = React.useState(null) + const [editingCell, setEditingCell] = React.useState(null) + const [editingValue, setEditingValue] = React.useState(null) + + // Check if any column is currently being resized + const isResizing = table.getState().columnSizingInfo.isResizingColumn !== false + + const handleCellClick = React.useCallback( + (rowId: string, columnId: string) => { + // Clear editing when clicking elsewhere + if (editingCell) { + setEditingCell(null) + setEditingValue(null) + } + + setSelectedCell((prev) => { + // Toggle off if clicking the same cell + if (prev?.rowId === rowId && prev?.columnId === columnId) { + return null + } + return { rowId, columnId } + }) + }, + [editingCell] + ) + + const handleCellDoubleClick = React.useCallback( + (rowIndex: number, columnId: string) => { + if (!onCellEditCommit) return + + // Get the current value + const row = table.getRowModel().rows[rowIndex] + if (!row) return + + // Check if there's already a pending edit for this cell + const existingEdit = cellEdits.find((e) => e.rowIndex === rowIndex && e.columnName === columnId) + const currentValue = existingEdit ? existingEdit.newValue : row.original[columnId] + + setEditingCell({ rowIndex, columnId }) + setEditingValue(currentValue) + setSelectedCell(null) + }, + [onCellEditCommit, table, cellEdits] + ) + + const handleEditChange = React.useCallback((value: unknown) => { + setEditingValue(value) + }, []) + + const handleEditCommit = React.useCallback(() => { + if (editingCell && onCellEditCommit) { + onCellEditCommit(editingCell.rowIndex, editingCell.columnId, editingValue) + } + setEditingCell(null) + setEditingValue(null) + }, [editingCell, editingValue, onCellEditCommit]) + + const handleEditCancel = React.useCallback(() => { + if (editingCell && onCellEditCancel) { + onCellEditCancel(editingCell.rowIndex, editingCell.columnId) + } + setEditingCell(null) + setEditingValue(null) + }, [editingCell, onCellEditCancel]) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isCheckboxColumn = header.column.id === 'select' + const isResizable = header.column.getCanResize() + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {isResizable && !isCheckboxColumn && ( +
+ )} + + ) + })} + + ))} + + + {/* Render editable rows */} + {editableRows?.map((editableRow) => ( + + ))} + + {/* Render regular data rows */} + {table.getRowModel().rows?.length > 0 && + table + .getRowModel() + .rows?.map((row, index) => ( + + ))} + +
+ {table.getRowModel().rows?.length === 0 && ( +
+
+
+
No rows
+
limit {pagination.limit}
+
offset {pagination.offset}
+
+
+
+ )} +
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DataTablePagination.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DataTablePagination.tsx new file mode 100644 index 0000000000..452c1bc3b9 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DataTablePagination.tsx @@ -0,0 +1,128 @@ +import * as React from 'react' +import { ChevronDown } from 'lucide-react' +import { useParams } from 'react-router-dom' + +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' +import { useDataTable } from '@/contexts/DataTableContext' +import { MIN_LIMIT, MAX_LIMIT, MIN_OFFSET } from '@/lib/constants' +import { useDbLocalStorage } from '@/hooks/useLocalStorage' + +export const DataTablePagination = React.memo(function DataTablePagination() { + const { dbId } = useParams() + const { pagination, onPaginationChange } = useDataTable() + const [, saveLimitPreference] = useDbLocalStorage(dbId, 'pagination-limit', pagination.limit) + + // Pagination is always enabled now (no editMode in context anymore) + const isDisabled = false + + // Local state for input values (allows typing before applying) + const [limitInput, setLimitInput] = React.useState(String(pagination.limit)) + const [offsetInput, setOffsetInput] = React.useState(String(pagination.offset)) + + // Sync input values when pagination changes externally (from URL) + React.useEffect(() => { + setLimitInput(String(pagination.limit)) + setOffsetInput(String(pagination.offset)) + }, [pagination.limit, pagination.offset]) + + return ( +
+ + + + + + {isDisabled ? 'Finish editing to navigate' : 'Previous Page'} + + + + + { + setLimitInput(e.target.value) + }} + onBlur={(e) => { + const newLimit = Math.max(MIN_LIMIT, Math.min(MAX_LIMIT, parseInt(e.target.value) || MIN_LIMIT)) + setLimitInput(String(newLimit)) + saveLimitPreference(newLimit) + onPaginationChange({ limit: newLimit, offset: pagination.offset }) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.currentTarget.blur() + } + }} + className="h-9 w-20 text-center font-mono text-sm rounded-none border-r-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus-visible:z-10" + min={MIN_LIMIT} + max={MAX_LIMIT} + disabled={isDisabled} + /> + + + LIMIT + + + + + { + setOffsetInput(e.target.value) + }} + onBlur={(e) => { + const newOffset = Math.max(MIN_OFFSET, parseInt(e.target.value) || MIN_OFFSET) + setOffsetInput(String(newOffset)) + onPaginationChange({ limit: pagination.limit, offset: newOffset }) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.currentTarget.blur() + } + }} + className="h-9 w-20 text-center font-mono text-sm rounded-none border-r-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus-visible:z-10" + min={MIN_OFFSET} + disabled={isDisabled} + /> + + + OFFSET + + + + + + + + {isDisabled ? 'Finish editing to navigate' : 'Next Page'} + + +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DataTableToolbar.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DataTableToolbar.tsx new file mode 100644 index 0000000000..f5032963d7 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DataTableToolbar.tsx @@ -0,0 +1,138 @@ +import * as React from 'react' + +import { Badge } from '@/components/ui/badge' +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' +import { Skeleton } from '@/components/ui/skeleton' +import { formatRowCount } from '@/lib/utils' +import { FilterButton } from './FilterButton' +import { ColumnsDropdown } from './ColumnsDropdown' +import { DataTablePagination } from './DataTablePagination' +import { AddRowButton } from './AddRowButton' +import { useDataTable } from '@/contexts/DataTableContext' +import { Button } from '@/components/ui/button' +import { RefreshCw, Trash2, Save, X } from 'lucide-react' + +type DataTableToolbarProps = { + showFilterPanel: boolean + onFilterToggle: () => void + onAddFilter: () => void + onDeleteRows?: () => void + onAddRow?: () => void + onSaveChanges?: () => void + onDiscardChanges?: () => void + hasEditableRows?: boolean +} + +export const DataTableToolbar = React.memo(function DataTableToolbar({ + showFilterPanel, + onFilterToggle, + onAddFilter, + onDeleteRows, + onAddRow, + onSaveChanges, + onDiscardChanges, + hasEditableRows, +}: DataTableToolbarProps) { + const { table, rowCount, metadata, config, appliedFilters, onRefresh } = useDataTable() + + // Get selected row count + const selectedRowCount = table.getFilteredSelectedRowModel().rows.length + + const handleFilterToggle = React.useCallback(() => { + onFilterToggle() + if (!showFilterPanel && appliedFilters.length === 0) { + onAddFilter() + } + }, [showFilterPanel, appliedFilters.length, onAddFilter, onFilterToggle]) + + return ( +
+ {config.tableName && ( +
+

{config.tableName}

+ {config.tableRowCount != undefined ? ( + + + + {formatRowCount(config.tableRowCount)} rows + + + + {config.tableRowCount.toLocaleString()} total rows + + + ) : ( + + )} +
+ )} + {config.showControls && !hasEditableRows && ( + <> + + + + )} + + {onAddRow && } + + {onSaveChanges && hasEditableRows && ( + + )} + + {onDiscardChanges && hasEditableRows && ( + + )} + + {selectedRowCount > 0 && onDeleteRows && !hasEditableRows && ( + + )} + +
+ {metadata?.executionTimeMs !== undefined && ( +
+ {rowCount} rows ⋅ + {metadata.executionTimeMs}ms +
+ )} + + {config.showPagination && } +
+ {onRefresh && ( + + + + + + Refresh rows + + + )} +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DeleteRowsDialog.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DeleteRowsDialog.tsx new file mode 100644 index 0000000000..ec050129a5 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DeleteRowsDialog.tsx @@ -0,0 +1,207 @@ +import * as React from 'react' +import type { Row } from '@tanstack/react-table' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { SQLCodeBlock } from '@/components/ui/sql-code-block' +import { useDeleteRows } from '@/hooks/useDeleteRows' +import { useDataTable } from '@/contexts/DataTableContext' +import type { ColumnMetadata } from '@/lib/types' + +type DeleteRowsDialogProps = { + isOpen: boolean + onOpenChange: (isOpen: boolean) => void +} + +/** + * Extract primary key values from a row + */ +function extractPrimaryKeys(row: Row>, columns: ColumnMetadata[]): Record { + const pkColumns = columns.filter((col) => col.primaryKey) + const pkValues: Record = {} + + for (const col of pkColumns) { + pkValues[col.name] = row.original[col.name] + } + + return pkValues +} + +/** + * Format primary key values for display + */ +function formatPrimaryKeyDisplay(pk: Record): string { + const entries = Object.entries(pk) + if (entries.length === 0) return '(no primary key)' + return entries.map(([key, value]) => `${key}: ${value}`).join(', ') +} + +export function DeleteRowsDialog({ isOpen, onOpenChange }: DeleteRowsDialogProps) { + const { table, columns } = useDataTable() + const selectedRows = table.getFilteredSelectedRowModel().rows + const deleteRowsMutation = useDeleteRows() + const [errorMessage, setErrorMessage] = React.useState(null) + const [errorSql, setErrorSql] = React.useState(null) + + // Extract primary keys from all selected rows + const primaryKeysToDelete = React.useMemo(() => { + return selectedRows.map((row) => extractPrimaryKeys(row, columns)) + }, [selectedRows, columns]) + + // Check if table has primary keys + const hasPrimaryKeys = columns.some((col) => col.primaryKey) + + const handleDelete = React.useCallback(async () => { + if (!hasPrimaryKeys) { + console.error('Cannot delete rows: table has no primary key') + return + } + + console.log('Starting delete operation...') + try { + setErrorMessage(null) // Clear any previous errors + setErrorSql(null) + await deleteRowsMutation.mutateAsync(primaryKeysToDelete) + console.log('Delete successful, closing dialog') + onOpenChange(false) + } catch (error: unknown) { + // Show error message in alert dialog + const message = error instanceof Error ? error.message : 'An unknown error occurred' + // Extract SQL from error response if available + const sql = (error as { response?: { sql?: string } })?.response?.sql ?? null + console.log('Delete failed with error:', message, error) + setErrorMessage(message) + setErrorSql(sql) + console.error('Delete failed:', error) + // Keep dialog open to show error + } + }, [hasPrimaryKeys, deleteRowsMutation, primaryKeysToDelete, onOpenChange]) + + // Reset error when dialog opens/closes + React.useEffect(() => { + if (!isOpen) { + setErrorMessage(null) + setErrorSql(null) + } + }, [isOpen]) + + if (!hasPrimaryKeys) { + return ( + + + + Cannot Delete Rows + + This table has no primary key. Deleting rows requires a primary key to identify specific rows. + + + + Close + + + + ) + } + + // Show error dialog if deletion failed + if (errorMessage) { + return ( + + + + Delete Failed + + Failed to delete the selected {selectedRows.length === 1 ? 'row' : 'rows'}. + + + + {/* Show error message */} +
+
{errorMessage}
+
+ + {/* Show SQL query if available */} + {errorSql && } + + + { + setErrorMessage(null) + setErrorSql(null) + }} + > + Close + + { + e.preventDefault() // Prevent dialog from auto-closing + setErrorMessage(null) + setErrorSql(null) + // Retry the delete + handleDelete() + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Retry + + +
+
+ ) + } + + return ( + + + + + Delete {selectedRows.length} {selectedRows.length === 1 ? 'Row' : 'Rows'} + + + Are you sure you want to delete {selectedRows.length === 1 ? 'this row' : 'these rows'}? This action cannot + be undone. + + + + {/* Show preview of rows to be deleted (limit to first 5) */} + {selectedRows.length > 0 && ( +
+
+ {primaryKeysToDelete.slice(0, 5).map((pk, idx) => ( +
+ {formatPrimaryKeyDisplay(pk)} +
+ ))} + {selectedRows.length > 5 && ( +
+ ... and {selectedRows.length - 5} more {selectedRows.length - 5 === 1 ? 'row' : 'rows'} +
+ )} +
+
+ )} + + + Cancel + { + e.preventDefault() // Prevent dialog from auto-closing + handleDelete() + }} + disabled={deleteRowsMutation.isPending} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {deleteRowsMutation.isPending ? 'Deleting...' : 'Delete'} + + +
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/DiscardChangesDialog.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/DiscardChangesDialog.tsx new file mode 100644 index 0000000000..a6b648e5c1 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/DiscardChangesDialog.tsx @@ -0,0 +1,44 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' + +type DiscardChangesDialogProps = { + isOpen: boolean + onOpenChange: (isOpen: boolean) => void + onDiscard: () => void +} + +export function DiscardChangesDialog({ isOpen, onOpenChange, onDiscard }: DiscardChangesDialogProps) { + return ( + + + + Discard unsaved changes? + + You have unsaved changes. Are you sure you want to discard them? + + + + Continue Editing + { + e.preventDefault() + onDiscard() + onOpenChange(false) + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Discard + + + + + ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/EditableCell.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/EditableCell.tsx new file mode 100644 index 0000000000..61f7d62704 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/EditableCell.tsx @@ -0,0 +1,108 @@ +import * as React from 'react' +import type { ColumnMetadata } from '@/lib/types' +import { Input } from '@/components/ui/input' +import { + NullEmptySuggestion, + EMPTY_STRING_MARKER, + isEmptyStringMarker, + getDisplayValue, + type NullableValue, +} from './NullEmptyDropdown' + +type EditableCellProps = { + column: ColumnMetadata + value: unknown + onChange: (value: unknown) => void +} + +export const EditableCell = React.memo(function EditableCell({ column, value, onChange }: EditableCellProps) { + const inputRef = React.useRef(null) + const [isFocused, setIsFocused] = React.useState(false) + + // Track internal value that distinguishes null from empty string + const [internalValue, setInternalValue] = React.useState(() => { + // Initialize: null stays null, empty string gets marker, others stay as-is + if (value === null || value === undefined) return null + if (value === '') return EMPTY_STRING_MARKER + return value + }) + + // Sync internal value when external value changes + React.useEffect(() => { + if (value === null || value === undefined) { + setInternalValue(null) + } else if (value === '') { + setInternalValue(EMPTY_STRING_MARKER) + } else { + setInternalValue(value) + } + }, [value]) + + const handleValueChange = React.useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value + if (newValue === '') { + // When cleared, default to NULL + setInternalValue(null) + onChange(null) + } else { + setInternalValue(newValue) + onChange(newValue) + } + }, + [onChange] + ) + + const handleNullEmptyChange = React.useCallback( + (newValue: null | typeof EMPTY_STRING_MARKER) => { + setInternalValue(newValue) + // Convert marker to actual empty string for the parent + onChange(isEmptyStringMarker(newValue) ? '' : null) + }, + [onChange] + ) + + const isAutoIncrementPK = !!(column.primaryKey && column.autoIncrement) + const displayValue = getDisplayValue(internalValue) + const isNull = internalValue === null + const isEmpty = isEmptyStringMarker(internalValue) + const isEmptyValue = isNull || isEmpty + const showSuggestion = isFocused && isEmptyValue + + // Get the label to show when empty + const emptyLabel = isAutoIncrementPK && isNull ? 'DEFAULT' : isNull ? 'NULL' : isEmpty ? 'EMPTY STRING' : null + + // Determine input type based on column type + const inputType = React.useMemo(() => { + const normalizedType = column.type.toUpperCase() + if (normalizedType.includes('INT')) return 'number' + if (normalizedType.includes('REAL') || normalizedType.includes('FLOAT') || normalizedType.includes('DOUBLE')) + return 'number' + return 'text' + }, [column.type]) + + return ( +
+ {emptyLabel && ( + + {emptyLabel} + + )} + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + className="h-full w-full border-0 rounded-none bg-transparent px-4 py-2 text-xs md:text-xs font-mono focus-visible:ring-0 focus-visible:ring-offset-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> + +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/EditableRow.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/EditableRow.tsx new file mode 100644 index 0000000000..bb8dc762c9 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/EditableRow.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import type { ColumnMetadata } from '@/lib/types' +import type { Table } from '@tanstack/react-table' +import { TableRow, TableCell } from '@/components/ui/table' +import { X } from 'lucide-react' +import { EditableCell } from './EditableCell' + +type EditableRowProps = { + rowId: string + table: Table> + columns: ColumnMetadata[] + rowData: Record + onCellChange: (rowId: string, columnName: string, value: unknown) => void + onRemove: (rowId: string) => void +} + +export const EditableRow = React.memo(function EditableRow({ + rowId, + table, + columns, + rowData, + onCellChange, + onRemove, +}: EditableRowProps) { + // Get all column definitions from the table (including checkbox) + const allColumns = table.getAllColumns() + + return ( + + {allColumns.map((column) => { + const width = column.getSize() + const isCheckboxColumn = column.id === 'select' + + if (isCheckboxColumn) { + // Render red X for removing this row + return ( + +
+ +
+
+ ) + } + + // Find the corresponding column metadata + const columnMeta = columns.find((col) => col.name === column.id) + if (!columnMeta) return null + + return ( + + onCellChange(rowId, columnMeta.name, value)} + /> + + ) + })} +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/FilterButton.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/FilterButton.tsx new file mode 100644 index 0000000000..ef4288ac5a --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/FilterButton.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { Filter as FilterIcon } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' + +type FilterButtonProps = { + showFilters: boolean + activeFilterCount: number + onToggle: () => void + disabled?: boolean +} + +export const FilterButton = React.memo(function FilterButton({ + showFilters, + activeFilterCount, + onToggle, + disabled, +}: FilterButtonProps) { + return ( +
+ + {activeFilterCount > 0 && ( + + ! + + )} +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/FilterRow.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/FilterRow.tsx new file mode 100644 index 0000000000..7f11e6c08d --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/FilterRow.tsx @@ -0,0 +1,140 @@ +import * as React from 'react' +import { ChevronDown, X } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import type { Filter, ColumnMetadata } from '@/lib/types' +import { FILTER_OPERATORS } from '@/lib/types' + +type FilterRowProps = { + filter: Filter + columns: ColumnMetadata[] + isFirst: boolean + onApply: () => void + onUpdate: (updates: Partial) => void + onRemove: () => void +} + +export const FilterRow = React.memo(function FilterRow({ + filter, + columns, + isFirst, + onApply, + onUpdate, + onRemove, +}: FilterRowProps) { + const operatorMeta = FILTER_OPERATORS.find((op) => op.value === filter.operator) + const requiresValue = operatorMeta?.requiresValue ?? true + const isArrayValue = operatorMeta?.isArrayValue ?? false + + return ( +
+ {/* First filter shows "where", others show "and" */} + + {isFirst ? 'where' : 'and'} + + + {/* Column selector */} + + + + + + {columns.map((col) => ( + onUpdate({ column: col.name })} + onSelect={(e) => e.preventDefault()} + > + {col.name} + + ))} + + + + {/* Operator selector */} + + + + + + {FILTER_OPERATORS.map((op) => ( + onUpdate({ operator: op.value })} + onSelect={(e) => e.preventDefault()} + className="pr-2" + > +
+ {op.label} + + {op.symbol} + +
+
+ ))} +
+
+ + {/* Value input (conditional based on operator) */} + {requiresValue && !isArrayValue && ( + onUpdate({ value: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onApply() + } + }} + className="h-8 w-[200px] text-xs md:text-xs font-mono" + /> + )} + + {/* Array value input for 'in' operator */} + {requiresValue && isArrayValue && ( + { + const values = e.target.value + .split(',') + .map((v) => v.trim()) + .filter((v) => v !== '') + onUpdate({ value: values }) + }} + className="h-8 w-[280px] text-xs md:text-xs font-mono" + /> + )} + + {/* Remove button */} + +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/InlineEditableCell.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/InlineEditableCell.tsx new file mode 100644 index 0000000000..7fe41ac010 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/InlineEditableCell.tsx @@ -0,0 +1,159 @@ +import * as React from 'react' +import type { ColumnMetadata } from '@/lib/types' +import { Input } from '@/components/ui/input' +import { + NullEmptySuggestion, + EMPTY_STRING_MARKER, + isEmptyStringMarker, + getDisplayValue, + type NullableValue, +} from './NullEmptyDropdown' + +type InlineEditableCellProps = { + column: ColumnMetadata + value: unknown + originalValue: unknown + onChange: (value: unknown) => void + onCommit: () => void + onCancel: () => void + autoFocus?: boolean +} + +export const InlineEditableCell = React.memo(function InlineEditableCell({ + column, + value, + onChange, + onCommit, + onCancel, + autoFocus = true, +}: InlineEditableCellProps) { + const inputRef = React.useRef(null) + const [isFocused, setIsFocused] = React.useState(autoFocus) + + // Track internal value that distinguishes null from empty string + const [internalValue, setInternalValue] = React.useState(() => { + if (value === null || value === undefined) return null + if (value === '') return EMPTY_STRING_MARKER + return value + }) + + // Sync internal value when external value changes + React.useEffect(() => { + if (value === null || value === undefined) { + setInternalValue(null) + } else if (value === '') { + setInternalValue(EMPTY_STRING_MARKER) + } else { + setInternalValue(value) + } + }, [value]) + + // Focus and select all on mount + React.useEffect(() => { + if (autoFocus && inputRef.current) { + inputRef.current.focus() + inputRef.current.select() + } + }, [autoFocus]) + + const handleValueChange = React.useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value + if (newValue === '') { + // When cleared, default to NULL + setInternalValue(null) + onChange(null) + } else { + setInternalValue(newValue) + onChange(newValue) + } + }, + [onChange] + ) + + const handleNullEmptyChange = React.useCallback( + (newValue: null | typeof EMPTY_STRING_MARKER) => { + setInternalValue(newValue) + // Convert marker to actual empty string for the parent + onChange(isEmptyStringMarker(newValue) ? '' : null) + // Refocus the input after selection + inputRef.current?.focus() + }, + [onChange] + ) + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + onCommit() + } else if (e.key === 'Escape') { + e.preventDefault() + onCancel() + } + }, + [onCommit, onCancel] + ) + + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + // Don't commit if focus is moving to the suggestion dropdown + const relatedTarget = e.relatedTarget as HTMLElement | null + if (relatedTarget?.closest('[data-null-empty-suggestion]')) { + return + } + setIsFocused(false) + onCommit() + }, + [onCommit] + ) + + const handleFocus = React.useCallback(() => { + setIsFocused(true) + }, []) + + const isAutoIncrementPK = !!(column.primaryKey && column.autoIncrement) + const displayValue = getDisplayValue(internalValue) + const isNull = internalValue === null + const isEmpty = isEmptyStringMarker(internalValue) + const isEmptyValue = isNull || isEmpty + const showSuggestion = isFocused && isEmptyValue + + // Get the label to show when empty + const emptyLabel = isAutoIncrementPK && isNull ? 'DEFAULT' : isNull ? 'NULL' : isEmpty ? 'EMPTY STRING' : null + + // Determine input type based on column type + const inputType = React.useMemo(() => { + const normalizedType = column.type.toUpperCase() + if (normalizedType.includes('INT')) return 'number' + if (normalizedType.includes('REAL') || normalizedType.includes('FLOAT') || normalizedType.includes('DOUBLE')) + return 'number' + return 'text' + }, [column.type]) + + return ( +
+ {emptyLabel && ( + + {emptyLabel} + + )} + + +
+ ) +}) diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/InsertRowDialog.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/InsertRowDialog.tsx new file mode 100644 index 0000000000..a0febe15c9 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/InsertRowDialog.tsx @@ -0,0 +1,123 @@ +import * as React from 'react' +import { CheckCircle2, XCircle, ChevronDown, ChevronRight } from 'lucide-react' +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Item, ItemMedia, ItemContent, ItemTitle } from '@/components/ui/item' +import { SQLCodeBlock } from '@/components/ui/sql-code-block' +import { cn } from '@/lib/utils' + +export type InsertResult = { + id: string + status: 'success' | 'error' + sql?: string + error?: string +} + +type InsertRowDialogProps = { + isOpen: boolean + onOpenChange: (isOpen: boolean) => void + results: InsertResult[] + onClose: () => void +} + +function SQLResultItem({ result }: { result: InsertResult }) { + const [expanded, setExpanded] = React.useState(false) + const isError = result.status === 'error' + const isSuccess = result.status === 'success' + + // Show SQL in syntax-highlighted block when expanded + const showExpandable = !!result.sql || isError + + return ( + showExpandable && setExpanded(!expanded)} + > + + {isSuccess && } + {isError && } + + + + {!expanded ? ( + + {/* SQL on one line, truncated */} + {result.sql || 'Insert row'} + + ) : ( +
+ {/* Show SQL with syntax highlighting when expanded */} + {result.sql && } + + {/* Show error message if there's an error */} + {isError && result.error && ( +
+
Error:
+
{result.error}
+
+ )} +
+ )} +
+ + {/* Expand/collapse indicator */} + {showExpandable && ( +
+ {expanded ? : } +
+ )} +
+ ) +} + +export function InsertRowDialog({ isOpen, onOpenChange, results, onClose }: InsertRowDialogProps) { + const hasResults = results.length > 0 + const successCount = results.filter((r) => r.status === 'success').length + const errorCount = results.filter((r) => r.status === 'error').length + + const getDescription = () => { + if (successCount > 0 && errorCount > 0) { + return `${successCount} row${successCount !== 1 ? 's' : ''} inserted successfully, but ${errorCount} row${errorCount !== 1 ? 's' : ''} failed.` + } + if (errorCount > 0) { + return `All ${errorCount} row${errorCount !== 1 ? 's' : ''} failed to insert.` + } + return 'Review the results below.' + } + + return ( + + + + + + Insert Failed + + {getDescription()} + + + {/* Results list */} + {hasResults && ( +
+ {results.map((result) => ( + + ))} +
+ )} + + + Close + +
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/NullEmptyDropdown.tsx b/packages/cli/src/db-studio/ui/src/components/data-table/NullEmptyDropdown.tsx new file mode 100644 index 0000000000..70a59732be --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/NullEmptyDropdown.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' + +// Special marker to distinguish empty string from null +export const EMPTY_STRING_MARKER = Symbol('EMPTY_STRING') + +export type NullableValue = null | typeof EMPTY_STRING_MARKER | string | number | unknown + +export function isEmptyStringMarker(value: unknown): value is typeof EMPTY_STRING_MARKER { + return value === EMPTY_STRING_MARKER +} + +export function getDisplayValue(value: NullableValue): string { + if (value === null) return '' + if (isEmptyStringMarker(value)) return '' + return String(value ?? '') +} + +export function getPlaceholderText(value: NullableValue, isAutoIncrementPK: boolean): string { + if (isAutoIncrementPK && value === null) { + return 'DEFAULT' + } + if (value === null) return 'NULL' + if (isEmptyStringMarker(value)) return 'EMPTY STRING' + return '' +} + +type NullEmptySuggestionProps = { + value: NullableValue + onChange: (value: null | typeof EMPTY_STRING_MARKER) => void + visible: boolean + anchorRef: React.RefObject +} + +export function NullEmptySuggestion({ value, onChange, visible, anchorRef }: NullEmptySuggestionProps) { + const [position, setPosition] = React.useState({ top: 0, left: 0 }) + const isCurrentlyNull = value === null + const isCurrentlyEmpty = isEmptyStringMarker(value) + + // Calculate position based on anchor element + React.useEffect(() => { + if (visible && anchorRef.current) { + const rect = anchorRef.current.getBoundingClientRect() + setPosition({ + top: rect.bottom + 4, // 4px gap below the input + left: rect.left, + }) + } + }, [visible, anchorRef]) + + if (!visible) return null + + const dropdown = ( +
e.preventDefault()} // Prevent blur on click + > + + +
+ ) + + // Render in a portal to escape overflow:hidden containers + return ReactDOM.createPortal(dropdown, document.body) +} diff --git a/packages/cli/src/db-studio/ui/src/components/data-table/index.ts b/packages/cli/src/db-studio/ui/src/components/data-table/index.ts new file mode 100644 index 0000000000..2cbb4a86fd --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/data-table/index.ts @@ -0,0 +1,16 @@ +// Main components +export { DataTable } from './DataTable' +export type { CellEdit, EditableRowData } from './DataTable' +export { DataTableProvider, useDataTable } from '@/contexts/DataTableContext' +export type { DataTableContextValue } from '@/contexts/DataTableContext' + +// Sub-components (for advanced usage) +export { DataTableToolbar } from './DataTableToolbar' +export { DataTableFilterPanel } from './DataTableFilterPanel' +export { DataTableGrid } from './DataTableGrid' +export { DataTablePagination } from './DataTablePagination' +export { ColumnHeader } from './ColumnHeader' +export { ColumnsDropdown } from './ColumnsDropdown' +export { FilterButton } from './FilterButton' +export { FilterRow } from './FilterRow' +export { DeleteRowsDialog } from './DeleteRowsDialog' diff --git a/packages/cli/src/db-studio/ui/src/components/table-editor/ColumnCard.tsx b/packages/cli/src/db-studio/ui/src/components/table-editor/ColumnCard.tsx new file mode 100644 index 0000000000..4308b1d09b --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/table-editor/ColumnCard.tsx @@ -0,0 +1,224 @@ +import { Controller, useWatch, useFormContext } from 'react-hook-form' +import type { Control } from 'react-hook-form' +import { Input } from '@/components/ui/input' +import { Switch } from '@/components/ui/switch' +import { Checkbox } from '@/components/ui/checkbox' +import { Button } from '@/components/ui/button' +import { Separator } from '@/components/ui/separator' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { columnTypes } from '@/lib/schemas/table-editor' +import type { TableEditorFormData } from '@/lib/schemas/table-editor' +import { generateColumnSql } from '@/lib/sql-utils' + +type ColumnCardProps = { + index: number + control: Control + isSelected: boolean + onToggleSelect: () => void + isEditMode: boolean + isNewColumn: boolean +} + +export function ColumnCard({ index, control, isSelected, onToggleSelect, isEditMode, isNewColumn }: ColumnCardProps) { + const { setValue } = useFormContext() + + // Watch all column fields to generate SQL preview + const column = useWatch({ + control, + name: `columns.${index}`, + }) + + const primaryKey = column?.primaryKey || false + const columnType = column?.type || 'TEXT' + + // Generate SQL preview + const sqlPreview = column ? generateColumnSql(column) : '' + + return ( +
+ e.stopPropagation()} + /> + + + +
+ {sqlPreview || New column...} +
+
+ + +
+ {/* Basic Properties */} +
+
+ + ( + + )} + /> +
+ +
+ + ( + + + + + + {columnTypes.map((type) => ( + field.onChange(type)} className="font-mono"> + {type} + + ))} + + + )} + /> +
+
+ + + + {/* Constraints */} +
+

Constraints

+ + {/* Not Null - when PK is on, always show as ON (NOT NULL) */} +
+
+ +

Column must not assume the null value

+
+ ( + field.onChange(!checked)} + disabled={(isEditMode && !isNewColumn) || primaryKey} + /> + )} + /> +
+ + {/* Primary Key */} +
+
+ +

+ Can be used as a unique identifier for rows in the table +

+
+ ( + { + field.onChange(checked) + // When PK is checked, set nullable to false and unique to true (PKs are implicitly NOT NULL and UNIQUE) + if (checked) { + setValue(`columns.${index}.nullable`, false) + setValue(`columns.${index}.unique`, true) + } + }} + disabled={isEditMode} + /> + )} + /> +
+ + {/* Auto Increment */} + {columnType === 'INTEGER' && ( +
+
+ +

Automatically generate unique incrementing values

+
+ ( + + )} + /> +
+ )} + + {/* Unique - when PK is on, always show as checked */} +
+
+ +

+ Ensure that the data contained in a column is unique among all the rows +

+
+ ( + + )} + /> +
+
+ + + + {/* Default Value */} +
+ + ( + { + const val = e.target.value.trim() + field.onChange(val === '' ? null : val) + }} + placeholder="NULL" + disabled={isEditMode && !isNewColumn} + /> + )} + /> +

+ Default value when none is specified. Use CURRENT_TIMESTAMP for timestamps. +

+
+
+
+
+
+
+ ) +} diff --git a/packages/cli/src/db-studio/ui/src/components/ui/accordion.tsx b/packages/cli/src/db-studio/ui/src/components/ui/accordion.tsx new file mode 100644 index 0000000000..e1797c9345 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/alert-dialog.tsx b/packages/cli/src/db-studio/ui/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000..d7ed199d01 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/alert-dialog.tsx @@ -0,0 +1,104 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/packages/cli/src/db-studio/ui/src/components/ui/badge.tsx b/packages/cli/src/db-studio/ui/src/components/ui/badge.tsx new file mode 100644 index 0000000000..1741bab8c0 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow", + secondary: + "border-transparent bg-secondary text-secondary-foreground", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/button.tsx b/packages/cli/src/db-studio/ui/src/components/ui/button.tsx new file mode 100644 index 0000000000..b6772089d1 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/button.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/card.tsx b/packages/cli/src/db-studio/ui/src/components/ui/card.tsx new file mode 100644 index 0000000000..5f0647f8e8 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/card.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/checkbox.tsx b/packages/cli/src/db-studio/ui/src/components/ui/checkbox.tsx new file mode 100644 index 0000000000..f9c27f4d7e --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +'use client' + +import * as React from 'react' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { Check } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/dropdown-menu.tsx b/packages/cli/src/db-studio/ui/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000000..a9daabd665 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,180 @@ +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0', + inset && 'pl-8', + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/packages/cli/src/db-studio/ui/src/components/ui/hover-card.tsx b/packages/cli/src/db-studio/ui/src/components/ui/hover-card.tsx new file mode 100644 index 0000000000..95643d9c70 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/hover-card.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/input.tsx b/packages/cli/src/db-studio/ui/src/components/ui/input.tsx new file mode 100644 index 0000000000..69b64fb245 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/item.tsx b/packages/cli/src/db-studio/ui/src/components/ui/item.tsx new file mode 100644 index 0000000000..93d2b8122d --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/item.tsx @@ -0,0 +1,193 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const itemVariants = cva( + "group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]", + { + variants: { + variant: { + default: "bg-transparent", + outline: "border-border", + muted: "bg-muted/50", + }, + size: { + default: "gap-4 p-4 ", + sm: "gap-2.5 px-4 py-3", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Item({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"div"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div" + return ( + + ) +} + +const itemMediaVariants = cva( + "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none", + { + variants: { + variant: { + default: "bg-transparent", + icon: "bg-muted size-8 rounded-sm border [&_svg:not([class*='size-'])]:size-4", + image: + "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function ItemMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function ItemContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function ItemActions({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ) +} + +function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Item, + ItemMedia, + ItemContent, + ItemActions, + ItemGroup, + ItemSeparator, + ItemTitle, + ItemDescription, + ItemHeader, + ItemFooter, +} diff --git a/packages/cli/src/db-studio/ui/src/components/ui/kbd.tsx b/packages/cli/src/db-studio/ui/src/components/ui/kbd.tsx new file mode 100644 index 0000000000..44d8ba842d --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/kbd.tsx @@ -0,0 +1,28 @@ +import { cn } from "@/lib/utils" + +function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { + return ( + + ) +} + +function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +export { Kbd, KbdGroup } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/resizable.tsx b/packages/cli/src/db-studio/ui/src/components/ui/resizable.tsx new file mode 100644 index 0000000000..cd3cb0ec78 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/resizable.tsx @@ -0,0 +1,43 @@ +import { GripVertical } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/separator.tsx b/packages/cli/src/db-studio/ui/src/components/ui/separator.tsx new file mode 100644 index 0000000000..275381cab0 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/packages/cli/src/db-studio/ui/src/components/ui/sheet.tsx b/packages/cli/src/db-studio/ui/src/components/ui/sheet.tsx new file mode 100644 index 0000000000..6906f5b2b1 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/sheet.tsx @@ -0,0 +1,137 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/packages/cli/src/db-studio/ui/src/components/ui/sidebar.tsx b/packages/cli/src/db-studio/ui/src/components/ui/sidebar.tsx new file mode 100644 index 0000000000..2fbd93fb56 --- /dev/null +++ b/packages/cli/src/db-studio/ui/src/components/ui/sidebar.tsx @@ -0,0 +1,636 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { PanelLeft } from 'lucide-react' + +import { useIsMobile } from '@/hooks/use-mobile' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { Skeleton } from '@/components/ui/skeleton' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' + +const SIDEBAR_COOKIE_NAME = 'sidebar_state' +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = '16rem' +const SIDEBAR_WIDTH_MOBILE = '18rem' +const SIDEBAR_WIDTH_ICON = '3rem' +const SIDEBAR_KEYBOARD_SHORTCUT = 'b' + +type SidebarContextProps = { + state: 'expanded' | 'collapsed' + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error('useSidebar must be used within a SidebarProvider.') + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? 'expanded' : 'collapsed' + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) +}) +SidebarProvider.displayName = 'SidebarProvider' + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + side?: 'left' | 'right' + variant?: 'sidebar' | 'floating' | 'inset' + collapsible?: 'offcanvas' | 'icon' | 'none' + } +>(({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas', className, children, ...props }, ref) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === 'none') { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +}) +Sidebar.displayName = 'Sidebar' + +const SidebarTrigger = React.forwardRef, React.ComponentProps>( + ({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + + ) + } +) +SidebarTrigger.displayName = 'SidebarTrigger' + +const SidebarRail = React.forwardRef>( + ({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( +