Compare commits
7 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0949ebc637 | |||
| 3e045f4c10 | |||
| 17db1e64e1 | |||
| d7ab553120 | |||
| 91592019c4 | |||
| 0e8f63101c | |||
| 4e2cad790b |
379
.claude/skills/jsdoc/SKILL.md
Normal file
379
.claude/skills/jsdoc/SKILL.md
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
---
|
||||||
|
name: jsdoc
|
||||||
|
description: Commenting and documentation guidelines. Auto-activate when the user discusses comments, documentation, docstrings, code clarity, API docs, JSDoc, or asks about commenting strategies.
|
||||||
|
---
|
||||||
|
|
||||||
|
Auto-activate when: User discusses comments, documentation, docstrings, code clarity, code quality, API docs, JSDoc, Python docstrings, or asks about commenting strategies.
|
||||||
|
Core Principle
|
||||||
|
|
||||||
|
Write code that speaks for itself. Comment only when necessary to explain WHY, not WHAT.
|
||||||
|
|
||||||
|
Most code does not need comments. Well-written code with clear naming and structure is self-documenting.
|
||||||
|
|
||||||
|
The best comment is the one you don't need to write because the code is already obvious.
|
||||||
|
The Commenting Philosophy
|
||||||
|
When to Comment
|
||||||
|
|
||||||
|
✅ DO comment when explaining:
|
||||||
|
|
||||||
|
WHY something is done (business logic, design decisions)
|
||||||
|
Complex algorithms and their reasoning
|
||||||
|
Non-obvious trade-offs or constraints
|
||||||
|
Workarounds for bugs or limitations
|
||||||
|
API contracts and public interfaces
|
||||||
|
Regex patterns and what they match
|
||||||
|
Performance considerations or optimizations
|
||||||
|
Constants and magic numbers
|
||||||
|
Gotchas or surprising behaviors
|
||||||
|
|
||||||
|
❌ DON'T comment when:
|
||||||
|
|
||||||
|
The code is obvious and self-explanatory
|
||||||
|
The comment repeats the code (redundant)
|
||||||
|
Better naming would eliminate the need
|
||||||
|
The comment would become outdated quickly
|
||||||
|
It's decorative or organizational noise
|
||||||
|
It states what a standard language construct does
|
||||||
|
|
||||||
|
Comment Anti-Patterns
|
||||||
|
❌ 1. Obvious Comments
|
||||||
|
|
||||||
|
BAD:
|
||||||
|
|
||||||
|
counter = 0 # Initialize counter to zero
|
||||||
|
counter += 1 # Increment counter by one
|
||||||
|
user_name = input("Enter name: ") # Get user name from input
|
||||||
|
|
||||||
|
Better: No comment needed - the code is self-explanatory.
|
||||||
|
❌ 2. Redundant Comments
|
||||||
|
|
||||||
|
BAD:
|
||||||
|
|
||||||
|
def get_user_name(user):
|
||||||
|
return user.name # Return the user's name
|
||||||
|
|
||||||
|
def calculate_total(items):
|
||||||
|
# Loop through items and sum the prices
|
||||||
|
total = 0
|
||||||
|
for item in items:
|
||||||
|
total += item.price
|
||||||
|
return total
|
||||||
|
|
||||||
|
Better:
|
||||||
|
|
||||||
|
def get_user_name(user):
|
||||||
|
return user.name
|
||||||
|
|
||||||
|
def calculate_total(items):
|
||||||
|
return sum(item.price for item in items)
|
||||||
|
|
||||||
|
❌ 3. Outdated Comments
|
||||||
|
|
||||||
|
BAD:
|
||||||
|
|
||||||
|
# Calculate tax at 5% rate
|
||||||
|
tax = price * 0.08 # Actually 8%, comment is wrong
|
||||||
|
|
||||||
|
# DEPRECATED: Use new_api_function() instead
|
||||||
|
def old_function(): # Still being used, comment is misleading
|
||||||
|
pass
|
||||||
|
|
||||||
|
Better: Keep comments in sync with code, or remove them entirely.
|
||||||
|
❌ 4. Noise Comments
|
||||||
|
|
||||||
|
BAD:
|
||||||
|
|
||||||
|
# Start of function
|
||||||
|
def calculate():
|
||||||
|
# Declare variable
|
||||||
|
result = 0
|
||||||
|
# Return result
|
||||||
|
return result
|
||||||
|
# End of function
|
||||||
|
|
||||||
|
Better: Remove all of these comments.
|
||||||
|
❌ 5. Dead Code & Changelog Comments
|
||||||
|
|
||||||
|
BAD:
|
||||||
|
|
||||||
|
# Don't comment out code - use version control
|
||||||
|
# def old_function():
|
||||||
|
# return "deprecated"
|
||||||
|
|
||||||
|
# Don't maintain history in comments
|
||||||
|
# Modified by John on 2023-01-15
|
||||||
|
# Fixed bug reported by Sarah on 2023-02-03
|
||||||
|
|
||||||
|
Better: Delete the code. Git has the history.
|
||||||
|
Good Comment Examples
|
||||||
|
✅ Complex Business Logic
|
||||||
|
|
||||||
|
# Apply progressive tax brackets: 10% up to $10k, 20% above
|
||||||
|
# This matches IRS publication 501 for 2024
|
||||||
|
def calculate_progressive_tax(income):
|
||||||
|
if income <= 10000:
|
||||||
|
return income * 0.10
|
||||||
|
else:
|
||||||
|
return 1000 + (income - 10000) * 0.20
|
||||||
|
|
||||||
|
✅ Non-obvious Algorithms
|
||||||
|
|
||||||
|
# Using Floyd-Warshall for all-pairs shortest paths
|
||||||
|
# because we need distances between all nodes.
|
||||||
|
# Time: O(n³), Space: O(n²)
|
||||||
|
for k in range(vertices):
|
||||||
|
for i in range(vertices):
|
||||||
|
for j in range(vertices):
|
||||||
|
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
|
||||||
|
|
||||||
|
✅ Regex Patterns
|
||||||
|
|
||||||
|
# Match email format: username@domain.extension
|
||||||
|
# Allows letters, numbers, dots, hyphens in username
|
||||||
|
# Requires valid domain and 2+ char extension
|
||||||
|
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
|
||||||
|
✅ API Constraints or Gotchas
|
||||||
|
|
||||||
|
# GitHub API rate limit: 5000 requests/hour for authenticated users
|
||||||
|
# We implement exponential backoff to handle rate limiting
|
||||||
|
await rate_limiter.wait()
|
||||||
|
response = await fetch(github_api_url)
|
||||||
|
|
||||||
|
✅ Workarounds for Bugs
|
||||||
|
|
||||||
|
# HACK: Workaround for bug in library v2.1.0
|
||||||
|
# Remove after upgrading to v2.2.0
|
||||||
|
# See: https://github.com/library/issues/123
|
||||||
|
if library_version == "2.1.0":
|
||||||
|
apply_workaround()
|
||||||
|
|
||||||
|
Decision Framework
|
||||||
|
|
||||||
|
Before writing a comment, ask yourself:
|
||||||
|
Step 1: Is the code self-explanatory?
|
||||||
|
|
||||||
|
If YES → No comment needed
|
||||||
|
If NO → Continue to step 2
|
||||||
|
|
||||||
|
Step 2: Would a better variable/function name eliminate the need?
|
||||||
|
|
||||||
|
If YES → Refactor the code instead
|
||||||
|
If NO → Continue to step 3
|
||||||
|
|
||||||
|
Step 3: Does this explain WHY, not WHAT?
|
||||||
|
|
||||||
|
If explaining WHAT → Refactor code to be clearer
|
||||||
|
If explaining WHY → Good comment candidate
|
||||||
|
|
||||||
|
Step 4: Will this help future maintainers?
|
||||||
|
|
||||||
|
If YES → Write the comment
|
||||||
|
If NO → Skip it
|
||||||
|
|
||||||
|
Special Cases for Comments
|
||||||
|
Public APIs and Docstrings
|
||||||
|
Python Docstrings
|
||||||
|
|
||||||
|
def calculate_compound_interest(
|
||||||
|
principal: float,
|
||||||
|
rate: float,
|
||||||
|
time: int,
|
||||||
|
compound_frequency: int = 1
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate compound interest using the standard formula.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
principal: Initial amount invested
|
||||||
|
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
|
||||||
|
time: Time period in years
|
||||||
|
compound_frequency: Times per year interest compounds (default: 1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Final amount after compound interest
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any parameter is negative
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> calculate_compound_interest(1000, 0.05, 10)
|
||||||
|
1628.89
|
||||||
|
"""
|
||||||
|
if principal < 0 or rate < 0 or time < 0:
|
||||||
|
raise ValueError("Parameters must be non-negative")
|
||||||
|
|
||||||
|
# Compound interest formula: A = P(1 + r/n)^(nt)
|
||||||
|
return principal * (1 + rate / compound_frequency) ** (compound_frequency * time)
|
||||||
|
|
||||||
|
JavaScript/TypeScript JSDoc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user data from the API.
|
||||||
|
*
|
||||||
|
* @param {string} userId - The unique user identifier
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
* @param {boolean} options.includeProfile - Include profile data (default: true)
|
||||||
|
* @param {number} options.timeout - Request timeout in ms (default: 5000)
|
||||||
|
*
|
||||||
|
* @returns {Promise<User>} User object with requested fields
|
||||||
|
*
|
||||||
|
* @throws {Error} If userId is invalid or request fails
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const user = await fetchUser('123', { includeProfile: true });
|
||||||
|
*/
|
||||||
|
async function fetchUser(userId, options = {}) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
Constants and Configuration
|
||||||
|
|
||||||
|
# Based on network reliability studies (95th percentile)
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
|
||||||
|
# AWS Lambda timeout is 15s, leaving 5s buffer for cleanup
|
||||||
|
API_TIMEOUT = 10000 # milliseconds
|
||||||
|
|
||||||
|
# Cache duration optimized for balance between freshness and load
|
||||||
|
# See: docs/performance-tuning.md
|
||||||
|
CACHE_TTL = 300 # 5 minutes
|
||||||
|
|
||||||
|
Annotations for TODOs and Warnings
|
||||||
|
|
||||||
|
# TODO: Replace with proper authentication after security review
|
||||||
|
# Issue: #456
|
||||||
|
def temporary_auth(user):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# WARNING: This function modifies the original array instead of creating a copy
|
||||||
|
def sort_in_place(arr):
|
||||||
|
arr.sort()
|
||||||
|
return arr
|
||||||
|
|
||||||
|
# FIXME: Memory leak in production - investigate connection pooling
|
||||||
|
# Ticket: JIRA-789
|
||||||
|
def get_connection():
|
||||||
|
return create_connection()
|
||||||
|
|
||||||
|
# PERF: Consider caching this result if called frequently in hot path
|
||||||
|
def expensive_calculation(data):
|
||||||
|
return complex_algorithm(data)
|
||||||
|
|
||||||
|
# SECURITY: Validate input to prevent SQL injection before using in query
|
||||||
|
def build_query(user_input):
|
||||||
|
sanitized = escape_sql(user_input)
|
||||||
|
return f"SELECT * FROM users WHERE name = '{sanitized}'"
|
||||||
|
|
||||||
|
Common Annotation Keywords
|
||||||
|
|
||||||
|
TODO: - Work that needs to be done
|
||||||
|
FIXME: - Known bugs that need fixing
|
||||||
|
HACK: - Temporary workarounds
|
||||||
|
NOTE: - Important information or context
|
||||||
|
WARNING: - Critical information about usage
|
||||||
|
PERF: - Performance considerations
|
||||||
|
SECURITY: - Security-related notes
|
||||||
|
BUG: - Known bug documentation
|
||||||
|
REFACTOR: - Code that needs refactoring
|
||||||
|
DEPRECATED: - Soon-to-be-removed code
|
||||||
|
|
||||||
|
Refactoring Over Commenting
|
||||||
|
Instead of Commenting Complex Code...
|
||||||
|
|
||||||
|
BAD: Complex code with comment
|
||||||
|
|
||||||
|
# Check if user is admin or has special permissions
|
||||||
|
if user.role == "admin" or (user.permissions and "special" in user.permissions):
|
||||||
|
grant_access()
|
||||||
|
|
||||||
|
...Extract to Named Function
|
||||||
|
|
||||||
|
GOOD: Self-explanatory through naming
|
||||||
|
|
||||||
|
def user_has_admin_access(user):
|
||||||
|
return user.role == "admin" or has_special_permission(user)
|
||||||
|
|
||||||
|
def has_special_permission(user):
|
||||||
|
return user.permissions and "special" in user.permissions
|
||||||
|
|
||||||
|
if user_has_admin_access(user):
|
||||||
|
grant_access()
|
||||||
|
|
||||||
|
Language-Specific Examples
|
||||||
|
JavaScript
|
||||||
|
|
||||||
|
// Good: Explains WHY we debounce
|
||||||
|
// Debounce search to reduce API calls (500ms wait after last keystroke)
|
||||||
|
const debouncedSearch = debounce(searchAPI, 500);
|
||||||
|
|
||||||
|
// Bad: Obvious
|
||||||
|
let count = 0; // Initialize count to zero
|
||||||
|
count++; // Increment count
|
||||||
|
|
||||||
|
// Good: Explains algorithm choice
|
||||||
|
// Using Set for O(1) lookup instead of Array.includes() which is O(n)
|
||||||
|
const seen = new Set(ids);
|
||||||
|
|
||||||
|
Python
|
||||||
|
|
||||||
|
# Good: Explains the algorithm choice
|
||||||
|
# Using binary search because data is sorted and we need O(log n) performance
|
||||||
|
index = bisect.bisect_left(sorted_list, target)
|
||||||
|
|
||||||
|
# Bad: Redundant
|
||||||
|
def get_total(items):
|
||||||
|
return sum(items) # Return the sum of items
|
||||||
|
|
||||||
|
# Good: Explains why we're doing this
|
||||||
|
# Extract to separate function for type checking in mypy
|
||||||
|
def validate_user(user):
|
||||||
|
if not user or not user.id:
|
||||||
|
raise ValueError("Invalid user")
|
||||||
|
return user
|
||||||
|
|
||||||
|
TypeScript
|
||||||
|
|
||||||
|
// Good: Explains the type assertion
|
||||||
|
// TypeScript can't infer this is never null after the check
|
||||||
|
const element = document.getElementById('app') as HTMLElement;
|
||||||
|
|
||||||
|
// Bad: Obvious
|
||||||
|
const sum = a + b; // Add a and b
|
||||||
|
|
||||||
|
// Good: Explains non-obvious behavior
|
||||||
|
// spread operator creates shallow copy; use JSON for deep copy
|
||||||
|
const newConfig = { ...config };
|
||||||
|
|
||||||
|
Comment Quality Checklist
|
||||||
|
|
||||||
|
Before committing, ensure your comments:
|
||||||
|
|
||||||
|
Explain WHY, not WHAT
|
||||||
|
Are grammatically correct and clear
|
||||||
|
Will remain accurate as code evolves
|
||||||
|
Add genuine value to code understanding
|
||||||
|
Are placed appropriately (above the code they describe)
|
||||||
|
Use proper spelling and professional language
|
||||||
|
Follow team conventions for annotation keywords
|
||||||
|
Could not be replaced by better naming or structure
|
||||||
|
Are not obvious statements about language features
|
||||||
|
Reference tickets/issues when applicable
|
||||||
|
|
||||||
|
Summary
|
||||||
|
|
||||||
|
Priority order:
|
||||||
|
|
||||||
|
Clear code - Self-explanatory through naming and structure
|
||||||
|
Good comments - Explain WHY when necessary
|
||||||
|
Documentation - API docs, docstrings for public interfaces
|
||||||
|
No comments - Better than bad comments that lie or clutter
|
||||||
|
|
||||||
|
Remember: Comments are a failure to make the code self-explanatory. Use them sparingly and wisely.
|
||||||
|
Key Takeaways
|
||||||
|
Goal Approach
|
||||||
|
Reduce comments Improve naming, extract functions, simplify logic
|
||||||
|
Improve clarity Use self-explanatory code structure, clear variable names
|
||||||
|
Document APIs Use docstrings/JSDoc for public interfaces
|
||||||
|
Explain WHY Comment only business logic, algorithms, workarounds
|
||||||
|
Maintain accuracy Update comments when code changes, or remove them
|
||||||
353
.claude/skills/typescript/SKILL.md
Normal file
353
.claude/skills/typescript/SKILL.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
---
|
||||||
|
name: typescript
|
||||||
|
description: TypeScript engineering guidelines based on Google's style guide. Use when writing, reviewing, or refactoring TypeScript code in this project.
|
||||||
|
---
|
||||||
|
|
||||||
|
Comprehensive guidelines for writing production-quality TypeScript based on Google's TypeScript Style Guide.
|
||||||
|
Naming Conventions
|
||||||
|
Type Convention Example
|
||||||
|
Classes, Interfaces, Types, Enums UpperCamelCase UserService, HttpClient
|
||||||
|
Variables, Parameters, Functions lowerCamelCase userName, processData
|
||||||
|
Global Constants, Enum Values CONSTANT_CASE MAX_RETRIES, Status.ACTIVE
|
||||||
|
Type Parameters Single letter or UpperCamelCase T, ResponseType
|
||||||
|
Naming Principles
|
||||||
|
|
||||||
|
Descriptive names, avoid ambiguous abbreviations
|
||||||
|
Treat acronyms as words: loadHttpUrl not loadHTTPURL
|
||||||
|
No prefixes like opt_ for optional parameters
|
||||||
|
No trailing underscores for private properties
|
||||||
|
Single-letter variables only when scope is <10 lines
|
||||||
|
|
||||||
|
Variable Declarations
|
||||||
|
|
||||||
|
// Always use const by default
|
||||||
|
const users = getUsers();
|
||||||
|
|
||||||
|
// Use let only when reassignment is needed
|
||||||
|
let count = 0;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Never use var
|
||||||
|
// var x = 1; // WRONG
|
||||||
|
|
||||||
|
// One variable per declaration
|
||||||
|
const a = 1;
|
||||||
|
const b = 2;
|
||||||
|
// const a = 1, b = 2; // WRONG
|
||||||
|
|
||||||
|
Types and Interfaces
|
||||||
|
Prefer Interfaces Over Type Aliases
|
||||||
|
|
||||||
|
// Good: interface for object shapes
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid: type alias for object shapes
|
||||||
|
type User = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type aliases OK for unions, intersections, mapped types
|
||||||
|
type Status = 'active' | 'inactive';
|
||||||
|
type Combined = TypeA & TypeB;
|
||||||
|
|
||||||
|
Type Inference
|
||||||
|
|
||||||
|
Leverage inference for trivially inferred types:
|
||||||
|
|
||||||
|
// Good: inference is clear
|
||||||
|
const name = 'Alice';
|
||||||
|
const items = [1, 2, 3];
|
||||||
|
|
||||||
|
// Good: explicit for complex expressions
|
||||||
|
const result: ProcessedData = complexTransformation(input);
|
||||||
|
|
||||||
|
Array Types
|
||||||
|
|
||||||
|
// Simple types: use T[]
|
||||||
|
const numbers: number[];
|
||||||
|
const names: readonly string[];
|
||||||
|
|
||||||
|
// Multi-dimensional: use T[][]
|
||||||
|
const matrix: number[][];
|
||||||
|
|
||||||
|
// Complex types: use Array<T>
|
||||||
|
const handlers: Array<(event: Event) => void>;
|
||||||
|
|
||||||
|
Null and Undefined
|
||||||
|
|
||||||
|
// Prefer optional fields over union with undefined
|
||||||
|
interface Config {
|
||||||
|
timeout?: number; // Good
|
||||||
|
// timeout: number | undefined; // Avoid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type aliases must NOT include |null or |undefined
|
||||||
|
type UserId = string; // Good
|
||||||
|
// type UserId = string | null; // WRONG
|
||||||
|
|
||||||
|
// May use == for null comparison (catches both null and undefined)
|
||||||
|
if (value == null) {
|
||||||
|
// handles both null and undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
Types to Avoid
|
||||||
|
|
||||||
|
// Avoid any - use unknown instead
|
||||||
|
function parse(input: unknown): Data { }
|
||||||
|
|
||||||
|
// Avoid {} - use unknown, Record<string, T>, or object
|
||||||
|
function process(obj: Record<string, unknown>): void { }
|
||||||
|
|
||||||
|
// Use lowercase primitives
|
||||||
|
let name: string; // Good
|
||||||
|
// let name: String; // WRONG
|
||||||
|
|
||||||
|
// Never use wrapper objects
|
||||||
|
// new String('hello') // WRONG
|
||||||
|
|
||||||
|
Classes
|
||||||
|
Structure
|
||||||
|
|
||||||
|
class UserService {
|
||||||
|
// Fields first, initialized where declared
|
||||||
|
private readonly cache = new Map<string, User>();
|
||||||
|
private lastAccess: Date | null = null;
|
||||||
|
|
||||||
|
// Constructor with parameter properties
|
||||||
|
constructor(
|
||||||
|
private readonly api: ApiClient,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Methods separated by blank lines
|
||||||
|
async getUser(id: string): Promise<User> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateId(id: string): boolean {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Visibility
|
||||||
|
|
||||||
|
class Example {
|
||||||
|
// private by default, only use public when needed externally
|
||||||
|
private internalState = 0;
|
||||||
|
|
||||||
|
// readonly for properties never reassigned after construction
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
// Never use #private syntax - use TypeScript visibility
|
||||||
|
// #field = 1; // WRONG
|
||||||
|
private field = 1; // Good
|
||||||
|
}
|
||||||
|
|
||||||
|
Avoid Arrow Functions as Properties
|
||||||
|
|
||||||
|
class Handler {
|
||||||
|
// Avoid: arrow function as property
|
||||||
|
// handleClick = () => { ... };
|
||||||
|
|
||||||
|
// Good: instance method
|
||||||
|
handleClick(): void {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind at call site if needed
|
||||||
|
element.addEventListener('click', () => handler.handleClick());
|
||||||
|
|
||||||
|
Static Methods
|
||||||
|
|
||||||
|
Never use this in static methods
|
||||||
|
Call on defining class, not subclasses
|
||||||
|
|
||||||
|
Functions
|
||||||
|
Prefer Function Declarations
|
||||||
|
|
||||||
|
// Good: function declaration for named functions
|
||||||
|
function processData(input: Data): Result {
|
||||||
|
return transform(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow functions when type annotation needed
|
||||||
|
const handler: EventHandler = (event) => {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
Arrow Function Bodies
|
||||||
|
|
||||||
|
// Concise body only when return value is used
|
||||||
|
const double = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Block body when return should be void
|
||||||
|
const log = (msg: string) => {
|
||||||
|
console.log(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
|
||||||
|
// Use rest parameters, not arguments
|
||||||
|
function sum(...numbers: number[]): number {
|
||||||
|
return numbers.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructuring for multiple optional params
|
||||||
|
interface Options {
|
||||||
|
timeout?: number;
|
||||||
|
retries?: number;
|
||||||
|
}
|
||||||
|
function fetch(url: string, { timeout = 5000, retries = 3 }: Options = {}) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never name a parameter 'arguments'
|
||||||
|
|
||||||
|
Imports and Exports
|
||||||
|
Always Use Named Exports
|
||||||
|
|
||||||
|
// Good: named exports
|
||||||
|
export function processData() { }
|
||||||
|
export class UserService { }
|
||||||
|
export interface Config { }
|
||||||
|
|
||||||
|
// Never use default exports
|
||||||
|
// export default class UserService { } // WRONG
|
||||||
|
|
||||||
|
Import Styles
|
||||||
|
|
||||||
|
// Module import for large APIs
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// Named imports for frequently used symbols
|
||||||
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
|
|
||||||
|
// Type-only imports when only used as types
|
||||||
|
import type { User, Config } from './types';
|
||||||
|
|
||||||
|
Module Organization
|
||||||
|
|
||||||
|
Use modules, never namespace Foo { }
|
||||||
|
Never use require() - use ES6 imports
|
||||||
|
Use relative imports within same project
|
||||||
|
Avoid excessive ../../../
|
||||||
|
|
||||||
|
Control Structures
|
||||||
|
Always Use Braces
|
||||||
|
|
||||||
|
// Good
|
||||||
|
if (condition) {
|
||||||
|
doSomething();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exception: single-line if
|
||||||
|
if (condition) return early;
|
||||||
|
|
||||||
|
Loops
|
||||||
|
|
||||||
|
// Prefer for...of for arrays
|
||||||
|
for (const item of items) {
|
||||||
|
process(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Object methods with for...of for objects
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never use unfiltered for...in on arrays
|
||||||
|
|
||||||
|
Equality
|
||||||
|
|
||||||
|
// Always use === and !==
|
||||||
|
if (a === b) { }
|
||||||
|
|
||||||
|
// Exception: == null catches both null and undefined
|
||||||
|
if (value == null) { }
|
||||||
|
|
||||||
|
Switch Statements
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case Status.Active:
|
||||||
|
handleActive();
|
||||||
|
break;
|
||||||
|
case Status.Inactive:
|
||||||
|
handleInactive();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Always include default, even if empty
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception Handling
|
||||||
|
|
||||||
|
// Always throw Error instances
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
// throw 'error'; // WRONG
|
||||||
|
|
||||||
|
// Catch with unknown type
|
||||||
|
try {
|
||||||
|
riskyOperation();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
logger.error(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty catch needs justification comment
|
||||||
|
try {
|
||||||
|
optional();
|
||||||
|
} catch {
|
||||||
|
// Intentionally ignored: fallback behavior handles this
|
||||||
|
}
|
||||||
|
|
||||||
|
Type Assertions
|
||||||
|
|
||||||
|
// Use 'as' syntax, not angle brackets
|
||||||
|
const input = value as string;
|
||||||
|
// const input = <string>value; // WRONG in TSX, avoid everywhere
|
||||||
|
|
||||||
|
// Double assertion through unknown when needed
|
||||||
|
const config = (rawData as unknown) as Config;
|
||||||
|
|
||||||
|
// Add comment explaining why assertion is safe
|
||||||
|
const element = document.getElementById('app') as HTMLElement;
|
||||||
|
// Safe: element exists in index.html
|
||||||
|
|
||||||
|
Strings
|
||||||
|
|
||||||
|
// Use single quotes for string literals
|
||||||
|
const name = 'Alice';
|
||||||
|
|
||||||
|
// Template literals for interpolation or multiline
|
||||||
|
const message = `Hello, ${name}!`;
|
||||||
|
const query = `
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Never use backslash line continuations
|
||||||
|
|
||||||
|
Disallowed Features
|
||||||
|
Feature Alternative
|
||||||
|
var const or let
|
||||||
|
Array() constructor [] literal
|
||||||
|
Object() constructor {} literal
|
||||||
|
any type unknown
|
||||||
|
namespace modules
|
||||||
|
require() import
|
||||||
|
Default exports Named exports
|
||||||
|
#private fields private modifier
|
||||||
|
eval() Never use
|
||||||
|
const enum Regular enum
|
||||||
|
debugger Remove before commit
|
||||||
|
with Never use
|
||||||
|
Prototype modification Never modify
|
||||||
107
package-lock.json
generated
107
package-lock.json
generated
@@ -16,7 +16,6 @@
|
|||||||
"@floating-ui/react-dom": "^2.1.7",
|
"@floating-ui/react-dom": "^2.1.7",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
"immer": "^11.1.4",
|
"immer": "^11.1.4",
|
||||||
"jsdoc": "^4.0.5",
|
"jsdoc": "^4.0.5",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
"motion": "^12.38.0",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"qs": "^6.15.0",
|
"qs": "^6.15.0",
|
||||||
@@ -4612,34 +4612,11 @@
|
|||||||
"integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
|
"integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-node-resolve": {
|
|
||||||
"version": "16.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
|
||||||
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@rollup/pluginutils": "^5.0.1",
|
|
||||||
"@types/resolve": "1.20.2",
|
|
||||||
"deepmerge": "^4.2.2",
|
|
||||||
"is-module": "^1.0.0",
|
|
||||||
"resolve": "^1.22.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"rollup": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/pluginutils": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
|
||||||
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
|
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.0",
|
"@types/estree": "^1.0.0",
|
||||||
@@ -6175,12 +6152,6 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/resolve": {
|
|
||||||
"version": "1.20.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
|
||||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||||
@@ -10760,6 +10731,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.38.0",
|
||||||
|
"motion-utils": "^12.36.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
@@ -12318,12 +12316,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-module": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/is-negative-zero": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||||
@@ -14568,6 +14560,47 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.36.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"codegen": "wait-on http-get://localhost:3000/graphql/health && graphql-codegen",
|
"codegen": "wait-on http-get://localhost:3000/graphql/health && graphql-codegen",
|
||||||
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
|
"codegen:watch": "graphql-codegen --config codegen.yml --watch",
|
||||||
|
"knip": "knip"
|
||||||
},
|
},
|
||||||
"author": "Rishi Ghan",
|
"author": "Rishi Ghan",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"immer": "^11.1.4",
|
"immer": "^11.1.4",
|
||||||
"jsdoc": "^4.0.5",
|
"jsdoc": "^4.0.5",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
"motion": "^12.38.0",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"qs": "^6.15.0",
|
"qs": "^6.15.0",
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"styled-components": "^6.3.11",
|
"styled-components": "^6.3.11",
|
||||||
"threetwo-ui-typings": "^1.0.14",
|
"threetwo-ui-typings": "^1.0.14",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"websocket": "^1.0.35",
|
"websocket": "^1.0.35",
|
||||||
@@ -109,7 +112,7 @@
|
|||||||
"@types/ellipsize": "^0.1.3",
|
"@types/ellipsize": "^0.1.3",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.5.2",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
@@ -138,7 +141,7 @@
|
|||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
"tui-jsdoc-template": "^1.2.2",
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.2",
|
||||||
"wait-on": "^9.0.4"
|
"wait-on": "^9.0.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ This folder houses all the components, utils and libraries that make up ThreeTwo
|
|||||||
|
|
||||||
It is based on React 18, and uses:
|
It is based on React 18, and uses:
|
||||||
|
|
||||||
1. _Redux_ for state management
|
1. _zustand_ for state management
|
||||||
2. _socket.io_ for transferring data in real-time
|
2. _socket.io_ for transferring data in real-time
|
||||||
3. _React Router_ for routing
|
3. _React Router_ for routing
|
||||||
4. React DnD for drag-and-drop
|
4. React DnD for drag-and-drop
|
||||||
5. @tanstack/react-table for all tables
|
5. @tanstack/react-table for all tables
|
||||||
|
6. @tanstack/react-query for API calls
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, ReactElement, useCallback } from "react";
|
import React, { useState, ReactElement, useCallback, useMemo } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Card from "../shared/Carda";
|
import Card from "../shared/Carda";
|
||||||
import { RawFileDetails } from "./RawFileDetails";
|
import { RawFileDetails } from "./RawFileDetails";
|
||||||
@@ -10,7 +10,7 @@ import "react-sliding-pane/dist/react-sliding-pane.css";
|
|||||||
import SlidingPane from "react-sliding-pane";
|
import SlidingPane from "react-sliding-pane";
|
||||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||||
import { styled } from "styled-components";
|
import { styled } from "styled-components";
|
||||||
import { RawFileDetails as RawFileDetailsType } from "../../graphql/generated";
|
import type { RawFileDetails as RawFileDetailsType, InferredMetadata } from "../../graphql/generated";
|
||||||
|
|
||||||
// Extracted modules
|
// Extracted modules
|
||||||
import { useComicVineMatching } from "./useComicVineMatching";
|
import { useComicVineMatching } from "./useComicVineMatching";
|
||||||
@@ -23,52 +23,47 @@ const StyledSlidingPanel = styled(SlidingPane)`
|
|||||||
background: #ccc;
|
background: #ccc;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type InferredIssue = {
|
interface ComicVineMetadata {
|
||||||
name?: string;
|
name?: string;
|
||||||
number?: number;
|
volumeInformation?: Record<string, unknown>;
|
||||||
year?: string;
|
[key: string]: unknown;
|
||||||
subtitle?: string;
|
}
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ComicVineMetadata = {
|
interface Acquisition {
|
||||||
name?: string;
|
|
||||||
volumeInformation?: any;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Acquisition = {
|
|
||||||
directconnect?: {
|
directconnect?: {
|
||||||
downloads?: any[];
|
downloads?: unknown[];
|
||||||
};
|
};
|
||||||
torrent?: any[];
|
torrent?: unknown[];
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
};
|
}
|
||||||
|
|
||||||
type ComicDetailProps = {
|
interface ComicDetailProps {
|
||||||
data: {
|
data: {
|
||||||
_id: string;
|
_id: string;
|
||||||
rawFileDetails?: RawFileDetailsType;
|
rawFileDetails?: RawFileDetailsType;
|
||||||
inferredMetadata: {
|
inferredMetadata: InferredMetadata;
|
||||||
issue?: InferredIssue;
|
|
||||||
};
|
|
||||||
sourcedMetadata: {
|
sourcedMetadata: {
|
||||||
comicvine?: ComicVineMetadata;
|
comicvine?: ComicVineMetadata;
|
||||||
locg?: any;
|
locg?: Record<string, unknown>;
|
||||||
comicInfo?: any;
|
comicInfo?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
acquisition?: Acquisition;
|
acquisition?: Acquisition;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
userSettings?: any;
|
userSettings?: Record<string, unknown>;
|
||||||
queryClient?: any;
|
queryClient?: unknown;
|
||||||
comicObjectId?: string;
|
comicObjectId?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays full comic detail: cover, file info, action menu, and tabbed panels
|
* Displays full comic detail: cover, file info, action menu, and tabbed panels
|
||||||
* for metadata, archive operations, and acquisition.
|
* for metadata, archive operations, and acquisition.
|
||||||
|
*
|
||||||
|
* @param data.queryClient - react-query client passed through to the CV match
|
||||||
|
* panel so it can invalidate queries after a match is applied.
|
||||||
|
* @param data.comicObjectId - optional override for the comic ID; used when the
|
||||||
|
* component is rendered outside a route that provides the ID via `useParams`.
|
||||||
*/
|
*/
|
||||||
export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
||||||
const {
|
const {
|
||||||
@@ -104,7 +99,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
setVisible(true);
|
setVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Action menu handler
|
// Hide "match on Comic Vine" when there are no raw file details — matching
|
||||||
|
// requires file metadata to seed the search query.
|
||||||
const Placeholder = components.Placeholder;
|
const Placeholder = components.Placeholder;
|
||||||
const filteredActionOptions = filter(actionOptions, (item) => {
|
const filteredActionOptions = filter(actionOptions, (item) => {
|
||||||
if (isUndefined(rawFileDetails)) {
|
if (isUndefined(rawFileDetails)) {
|
||||||
@@ -130,6 +126,11 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
const isComicBookMetadataAvailable =
|
const isComicBookMetadataAvailable =
|
||||||
!isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation);
|
!isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation);
|
||||||
|
|
||||||
|
const hasAnyMetadata =
|
||||||
|
isComicBookMetadataAvailable ||
|
||||||
|
!isEmpty(comicInfo) ||
|
||||||
|
!isNil(locg);
|
||||||
|
|
||||||
const areRawFileDetailsAvailable =
|
const areRawFileDetailsAvailable =
|
||||||
!isUndefined(rawFileDetails) && !isEmpty(rawFileDetails);
|
!isUndefined(rawFileDetails) && !isEmpty(rawFileDetails);
|
||||||
|
|
||||||
@@ -140,26 +141,29 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Query for airdc++
|
// Query for airdc++
|
||||||
const airDCPPQuery = {
|
const airDCPPQuery = useMemo(() => ({
|
||||||
issue: {
|
issue: { name: issueName },
|
||||||
name: issueName,
|
}), [issueName]);
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create tab configuration
|
// Create tab configuration
|
||||||
const tabGroup = createTabConfig({
|
const openReconcilePanel = useCallback(() => {
|
||||||
|
setSlidingPanelContentId("metadataReconciliation");
|
||||||
|
setVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tabGroup = useMemo(() => createTabConfig({
|
||||||
data: data.data,
|
data: data.data,
|
||||||
comicInfo,
|
hasAnyMetadata,
|
||||||
isComicBookMetadataAvailable,
|
|
||||||
areRawFileDetailsAvailable,
|
areRawFileDetailsAvailable,
|
||||||
airDCPPQuery,
|
airDCPPQuery,
|
||||||
comicObjectId: _id,
|
comicObjectId: _id,
|
||||||
userSettings,
|
userSettings,
|
||||||
issueName,
|
issueName,
|
||||||
acquisition,
|
acquisition,
|
||||||
});
|
onReconcileMetadata: openReconcilePanel,
|
||||||
|
}), [data.data, hasAnyMetadata, areRawFileDetailsAvailable, airDCPPQuery, _id, userSettings, issueName, acquisition, openReconcilePanel]);
|
||||||
|
|
||||||
const filteredTabs = tabGroup.filter((tab) => tab.shouldShow);
|
const filteredTabs = useMemo(() => tabGroup.filter((tab) => tab.shouldShow), [tabGroup]);
|
||||||
|
|
||||||
// Sliding panel content mapping
|
// Sliding panel content mapping
|
||||||
const renderSlidingPanelContent = () => {
|
const renderSlidingPanelContent = () => {
|
||||||
@@ -170,6 +174,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
rawFileDetails={rawFileDetails}
|
rawFileDetails={rawFileDetails}
|
||||||
inferredMetadata={inferredMetadata}
|
inferredMetadata={inferredMetadata}
|
||||||
comicVineMatches={comicVineMatches}
|
comicVineMatches={comicVineMatches}
|
||||||
|
// Prefer the route param; fall back to the data ID when rendered outside a route.
|
||||||
comicObjectId={comicObjectId || _id}
|
comicObjectId={comicObjectId || _id}
|
||||||
queryClient={queryClient}
|
queryClient={queryClient}
|
||||||
onMatchApplied={() => {
|
onMatchApplied={() => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import TextareaAutosize from "react-textarea-autosize";
|
|||||||
|
|
||||||
interface EditMetadataPanelProps {
|
interface EditMetadataPanelProps {
|
||||||
data: {
|
data: {
|
||||||
name?: string;
|
name?: string | null;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { convert } from "html-to-text";
|
|||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useGetComicByIdQuery } from "../../graphql/generated";
|
||||||
|
|
||||||
interface MatchResultProps {
|
interface MatchResultProps {
|
||||||
matchData: any;
|
matchData: any;
|
||||||
@@ -31,7 +32,7 @@ export const MatchResult = (props: MatchResultProps) => {
|
|||||||
// Invalidate and refetch the comic book metadata
|
// Invalidate and refetch the comic book metadata
|
||||||
if (props.queryClient) {
|
if (props.queryClient) {
|
||||||
await props.queryClient.invalidateQueries({
|
await props.queryClient.invalidateQueries({
|
||||||
queryKey: ["comicBookMetadata", comicObjectId],
|
queryKey: useGetComicByIdQuery.getKey({ id: comicObjectId }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,27 @@ import React from "react";
|
|||||||
import { ComicVineSearchForm } from "./ComicVineSearchForm";
|
import { ComicVineSearchForm } from "./ComicVineSearchForm";
|
||||||
import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
|
import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
|
||||||
import { EditMetadataPanel } from "./EditMetadataPanel";
|
import { EditMetadataPanel } from "./EditMetadataPanel";
|
||||||
import { RawFileDetails } from "../../graphql/generated";
|
import type { RawFileDetails, InferredMetadata } from "../../graphql/generated";
|
||||||
|
|
||||||
type InferredIssue = {
|
interface CVMatchesPanelProps {
|
||||||
name?: string;
|
|
||||||
number?: number;
|
|
||||||
year?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CVMatchesPanelProps = {
|
|
||||||
rawFileDetails?: RawFileDetails;
|
rawFileDetails?: RawFileDetails;
|
||||||
inferredMetadata: {
|
inferredMetadata: InferredMetadata;
|
||||||
issue?: InferredIssue;
|
|
||||||
};
|
|
||||||
comicVineMatches: any[];
|
comicVineMatches: any[];
|
||||||
comicObjectId: string;
|
comicObjectId: string;
|
||||||
queryClient: any;
|
queryClient: any;
|
||||||
onMatchApplied: () => void;
|
onMatchApplied: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sliding panel content for ComicVine match search.
|
||||||
|
*
|
||||||
|
* Renders a search form pre-populated from `rawFileDetails`, a preview of the
|
||||||
|
* inferred issue being searched for, and a list of ComicVine match candidates
|
||||||
|
* the user can apply to the comic.
|
||||||
|
*
|
||||||
|
* @param props.onMatchApplied - Called after the user selects and applies a match,
|
||||||
|
* allowing the parent to close the panel and refresh state.
|
||||||
|
*/
|
||||||
export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({
|
export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({
|
||||||
rawFileDetails,
|
rawFileDetails,
|
||||||
inferredMetadata,
|
inferredMetadata,
|
||||||
@@ -62,4 +62,4 @@ type EditMetadataPanelWrapperProps = {
|
|||||||
|
|
||||||
export const EditMetadataPanelWrapper: React.FC<EditMetadataPanelWrapperProps> = ({
|
export const EditMetadataPanelWrapper: React.FC<EditMetadataPanelWrapperProps> = ({
|
||||||
rawFileDetails,
|
rawFileDetails,
|
||||||
}) => <EditMetadataPanel data={rawFileDetails} />;
|
}) => <EditMetadataPanel data={rawFileDetails ?? {}} />;
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ export const TabControls = (props): ReactElement => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Suspense fallback={null}>
|
||||||
{filteredTabs.map(({ id, content }) => {
|
{filteredTabs.map(({ id, content }) => (
|
||||||
return currentActive === id ? content : null;
|
<React.Fragment key={id}>
|
||||||
})}
|
{currentActive === id ? content : null}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -131,10 +131,12 @@ export const ArchiveOperations = (props: { data: any }): ReactElement => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSuccess && shouldRefetchComicBookData) {
|
useEffect(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
|
if (isSuccess && shouldRefetchComicBookData) {
|
||||||
setShouldRefetchComicBookData(false);
|
queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] });
|
||||||
}
|
setShouldRefetchComicBookData(false);
|
||||||
|
}
|
||||||
|
}, [isSuccess, shouldRefetchComicBookData, queryClient]);
|
||||||
|
|
||||||
// sliding panel init
|
// sliding panel init
|
||||||
const contentForSlidingPanel: Record<string, { content: () => JSX.Element }> = {
|
const contentForSlidingPanel: Record<string, { content: () => JSX.Element }> = {
|
||||||
|
|||||||
@@ -1,15 +1,165 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement, useMemo, useState } from "react";
|
||||||
|
import { isEmpty, isNil } from "lodash";
|
||||||
|
import { Drawer } from "vaul";
|
||||||
import ComicVineDetails from "../ComicVineDetails";
|
import ComicVineDetails from "../ComicVineDetails";
|
||||||
|
|
||||||
export const VolumeInformation = (props): ReactElement => {
|
interface ComicVineMetadata {
|
||||||
|
volumeInformation?: Record<string, unknown>;
|
||||||
|
name?: string;
|
||||||
|
number?: string;
|
||||||
|
resource_type?: string;
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourcedMetadata {
|
||||||
|
comicvine?: ComicVineMetadata;
|
||||||
|
locg?: Record<string, unknown>;
|
||||||
|
comicInfo?: unknown;
|
||||||
|
metron?: unknown;
|
||||||
|
gcd?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeInformationData {
|
||||||
|
sourcedMetadata?: SourcedMetadata;
|
||||||
|
inferredMetadata?: { issue?: unknown };
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeInformationProps {
|
||||||
|
data: VolumeInformationData;
|
||||||
|
onReconcile?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sources stored under `sourcedMetadata` — excludes `inferredMetadata`, which is checked separately. */
|
||||||
|
const SOURCED_METADATA_KEYS = [
|
||||||
|
"comicvine",
|
||||||
|
"locg",
|
||||||
|
"comicInfo",
|
||||||
|
"metron",
|
||||||
|
"gcd",
|
||||||
|
];
|
||||||
|
|
||||||
|
const SOURCE_LABELS: Record<string, string> = {
|
||||||
|
comicvine: "ComicVine",
|
||||||
|
locg: "League of Comic Geeks",
|
||||||
|
comicInfo: "ComicInfo.xml",
|
||||||
|
metron: "Metron",
|
||||||
|
gcd: "Grand Comics Database",
|
||||||
|
inferredMetadata: "Local File",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOURCE_ICONS: Record<string, string> = {
|
||||||
|
comicvine: "icon-[solar--database-bold]",
|
||||||
|
locg: "icon-[solar--users-group-rounded-outline]",
|
||||||
|
comicInfo: "icon-[solar--file-text-outline]",
|
||||||
|
metron: "icon-[solar--planet-outline]",
|
||||||
|
gcd: "icon-[solar--book-outline]",
|
||||||
|
inferredMetadata: "icon-[solar--folder-outline]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MetadataSourceChips = ({
|
||||||
|
sources,
|
||||||
|
}: {
|
||||||
|
sources: string[];
|
||||||
|
}): ReactElement => {
|
||||||
|
const [isSheetOpen, setSheetOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2 mb-5 p-3 w-fit">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-400">
|
||||||
|
<i className="icon-[solar--database-outline] w-4 h-4 inline-block align-middle mr-1" />
|
||||||
|
{sources.length} metadata sources detected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
|
{sources.map((source) => (
|
||||||
|
<span
|
||||||
|
key={source}
|
||||||
|
className="inline-flex items-center gap-1 bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-medium px-2 py-1 rounded-md border border-slate-200 dark:border-slate-600"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`${SOURCE_ICONS[source] ?? "icon-[solar--check-circle-outline]"} w-3 h-3`}
|
||||||
|
/>
|
||||||
|
{SOURCE_LABELS[source] ?? source}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="flex space-x-1 mb-2 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-2 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
|
onClick={() => setSheetOpen(true)}
|
||||||
|
>
|
||||||
|
<i className="icon-[solar--refresh-outline] w-4 h-4 px-3" />
|
||||||
|
Reconcile sources
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Drawer.Root open={isSheetOpen} onOpenChange={setSheetOpen}>
|
||||||
|
<Drawer.Portal>
|
||||||
|
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
|
||||||
|
<Drawer.Content aria-describedby={undefined} className="fixed bottom-0 left-0 right-0 rounded-t-2xl bg-white dark:bg-slate-800 p-4 outline-none">
|
||||||
|
<Drawer.Title className="sr-only">Reconcile metadata sources</Drawer.Title>
|
||||||
|
<div className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-slate-300 dark:bg-slate-600" />
|
||||||
|
<div className="p-4">
|
||||||
|
{/* Reconciliation UI goes here */}
|
||||||
|
</div>
|
||||||
|
</Drawer.Content>
|
||||||
|
</Drawer.Portal>
|
||||||
|
</Drawer.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays volume metadata for a comic.
|
||||||
|
*
|
||||||
|
* - When multiple sources are present, renders a chip bar listing each source
|
||||||
|
* with a "Reconcile sources" action to merge them.
|
||||||
|
* - When exactly one source is present and it is ComicVine, renders the full
|
||||||
|
* ComicVine detail panel directly.
|
||||||
|
*
|
||||||
|
* @param props.data - Comic data containing sourced and inferred metadata.
|
||||||
|
* @param props.onReconcile - Called when the user triggers source reconciliation.
|
||||||
|
*/
|
||||||
|
export const VolumeInformation = (
|
||||||
|
props: VolumeInformationProps,
|
||||||
|
): ReactElement => {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
|
||||||
|
const presentSources = useMemo(() => {
|
||||||
|
const sources = SOURCED_METADATA_KEYS.filter((key) => {
|
||||||
|
const val = (data?.sourcedMetadata ?? {})[key];
|
||||||
|
if (isNil(val) || isEmpty(val)) return false;
|
||||||
|
// locg returns an object even when empty; require at least one non-null value
|
||||||
|
if (key === "locg")
|
||||||
|
return Object.values(val as Record<string, unknown>).some(
|
||||||
|
(v) => !isNil(v) && v !== "",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!isNil(data?.inferredMetadata?.issue) &&
|
||||||
|
!isEmpty(data?.inferredMetadata?.issue)
|
||||||
|
) {
|
||||||
|
sources.push("inferredMetadata");
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}, [data?.sourcedMetadata, data?.inferredMetadata]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={1}>
|
<div key={1}>
|
||||||
<ComicVineDetails
|
{presentSources.length > 1 && (
|
||||||
data={data.sourcedMetadata.comicvine}
|
<MetadataSourceChips sources={presentSources} />
|
||||||
updatedAt={data.updatedAt}
|
)}
|
||||||
/>
|
{presentSources.length === 1 &&
|
||||||
|
data.sourcedMetadata?.comicvine?.volumeInformation && (
|
||||||
|
<ComicVineDetails
|
||||||
|
data={data.sourcedMetadata.comicvine}
|
||||||
|
updatedAt={data.updatedAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { lazy } from "react";
|
|||||||
import { isNil, isEmpty } from "lodash";
|
import { isNil, isEmpty } from "lodash";
|
||||||
|
|
||||||
const VolumeInformation = lazy(() => import("./Tabs/VolumeInformation").then(m => ({ default: m.VolumeInformation })));
|
const VolumeInformation = lazy(() => import("./Tabs/VolumeInformation").then(m => ({ default: m.VolumeInformation })));
|
||||||
const ComicInfoXML = lazy(() => import("./Tabs/ComicInfoXML").then(m => ({ default: m.ComicInfoXML })));
|
|
||||||
const ArchiveOperations = lazy(() => import("./Tabs/ArchiveOperations").then(m => ({ default: m.ArchiveOperations })));
|
const ArchiveOperations = lazy(() => import("./Tabs/ArchiveOperations").then(m => ({ default: m.ArchiveOperations })));
|
||||||
const AcquisitionPanel = lazy(() => import("./AcquisitionPanel"));
|
const AcquisitionPanel = lazy(() => import("./AcquisitionPanel"));
|
||||||
const TorrentSearchPanel = lazy(() => import("./TorrentSearchPanel"));
|
const TorrentSearchPanel = lazy(() => import("./TorrentSearchPanel"));
|
||||||
@@ -18,26 +17,26 @@ interface TabConfig {
|
|||||||
|
|
||||||
interface TabConfigParams {
|
interface TabConfigParams {
|
||||||
data: any;
|
data: any;
|
||||||
comicInfo: any;
|
hasAnyMetadata: boolean;
|
||||||
isComicBookMetadataAvailable: boolean;
|
|
||||||
areRawFileDetailsAvailable: boolean;
|
areRawFileDetailsAvailable: boolean;
|
||||||
airDCPPQuery: any;
|
airDCPPQuery: any;
|
||||||
comicObjectId: string;
|
comicObjectId: string;
|
||||||
userSettings: any;
|
userSettings: any;
|
||||||
issueName: string;
|
issueName: string;
|
||||||
acquisition?: any;
|
acquisition?: any;
|
||||||
|
onReconcileMetadata?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTabConfig = ({
|
export const createTabConfig = ({
|
||||||
data,
|
data,
|
||||||
comicInfo,
|
hasAnyMetadata,
|
||||||
isComicBookMetadataAvailable,
|
|
||||||
areRawFileDetailsAvailable,
|
areRawFileDetailsAvailable,
|
||||||
airDCPPQuery,
|
airDCPPQuery,
|
||||||
comicObjectId,
|
comicObjectId,
|
||||||
userSettings,
|
userSettings,
|
||||||
issueName,
|
issueName,
|
||||||
acquisition,
|
acquisition,
|
||||||
|
onReconcileMetadata,
|
||||||
}: TabConfigParams): TabConfig[] => {
|
}: TabConfigParams): TabConfig[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -46,23 +45,10 @@ export const createTabConfig = ({
|
|||||||
icon: (
|
icon: (
|
||||||
<i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
|
<i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
|
||||||
),
|
),
|
||||||
content: isComicBookMetadataAvailable ? (
|
content: hasAnyMetadata ? (
|
||||||
<VolumeInformation data={data} key={1} />
|
<VolumeInformation data={data} onReconcile={onReconcileMetadata} />
|
||||||
) : null,
|
) : null,
|
||||||
shouldShow: isComicBookMetadataAvailable,
|
shouldShow: hasAnyMetadata,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "ComicInfo.xml",
|
|
||||||
icon: (
|
|
||||||
<i className="h-5 w-5 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300" />
|
|
||||||
),
|
|
||||||
content: (
|
|
||||||
<div key={2}>
|
|
||||||
{!isNil(comicInfo) && <ComicInfoXML json={comicInfo} />}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
shouldShow: !isEmpty(comicInfo),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -70,7 +56,7 @@ export const createTabConfig = ({
|
|||||||
<i className="h-5 w-5 icon-[solar--winrar-bold-duotone] text-slate-500 dark:text-slate-300" />
|
<i className="h-5 w-5 icon-[solar--winrar-bold-duotone] text-slate-500 dark:text-slate-300" />
|
||||||
),
|
),
|
||||||
name: "Archive Operations",
|
name: "Archive Operations",
|
||||||
content: <ArchiveOperations data={data} key={3} />,
|
content: <ArchiveOperations data={data} />,
|
||||||
shouldShow: areRawFileDetailsAvailable,
|
shouldShow: areRawFileDetailsAvailable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,7 +71,6 @@ export const createTabConfig = ({
|
|||||||
comicObjectId={comicObjectId}
|
comicObjectId={comicObjectId}
|
||||||
comicObject={data}
|
comicObject={data}
|
||||||
settings={userSettings}
|
settings={userSettings}
|
||||||
key={4}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
shouldShow: true,
|
shouldShow: true,
|
||||||
@@ -112,7 +97,7 @@ export const createTabConfig = ({
|
|||||||
),
|
),
|
||||||
content:
|
content:
|
||||||
!isNil(data) && !isEmpty(data) ? (
|
!isNil(data) && !isEmpty(data) ? (
|
||||||
<DownloadsPanel key={5} />
|
<DownloadsPanel />
|
||||||
) : (
|
) : (
|
||||||
<div className="column is-three-fifths">
|
<div className="column is-three-fifths">
|
||||||
<article className="message is-info">
|
<article className="message is-info">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface LibraryStatisticsProps {
|
|||||||
* Returns `null` when `stats` is absent or the statistics array is empty.
|
* Returns `null` when `stats` is absent or the statistics array is empty.
|
||||||
*/
|
*/
|
||||||
export const LibraryStatistics = ({ stats }: LibraryStatisticsProps): ReactElement | null => {
|
export const LibraryStatistics = ({ stats }: LibraryStatisticsProps): ReactElement | null => {
|
||||||
if (!stats) return null;
|
if (!stats || !stats.totalDocuments) return null;
|
||||||
|
|
||||||
const facet = stats.statistics?.[0];
|
const facet = stats.statistics?.[0];
|
||||||
if (!facet) return null;
|
if (!facet) return null;
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ type VolumeGroupsProps = {
|
|||||||
volumeGroups?: GetVolumeGroupsQuery['getComicBookGroups'];
|
volumeGroups?: GetVolumeGroupsQuery['getComicBookGroups'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VolumeGroups = (props: VolumeGroupsProps): ReactElement => {
|
export const VolumeGroups = (props: VolumeGroupsProps): ReactElement | null => {
|
||||||
// Till mongo gives us back the deduplicated results with the ObjectId
|
// Till mongo gives us back the deduplicated results with the ObjectId
|
||||||
const deduplicatedGroups = unionBy(props.volumeGroups, "volumes.id");
|
const deduplicatedGroups = unionBy(props.volumeGroups, "volumes.id");
|
||||||
|
if (!deduplicatedGroups || deduplicatedGroups.length === 0) return null;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const navigateToVolumes = (row: any) => {
|
const navigateToVolumes = (row: any) => {
|
||||||
navigate(`/volumes/all`);
|
navigate(`/volumes/all`);
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ type WantedComicsListProps = {
|
|||||||
|
|
||||||
export const WantedComicsList = ({
|
export const WantedComicsList = ({
|
||||||
comics,
|
comics,
|
||||||
}: WantedComicsListProps): ReactElement => {
|
}: WantedComicsListProps): ReactElement | null => {
|
||||||
|
if (!comics || comics.length === 0) return null;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// embla carousel
|
// embla carousel
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["react", "react-dom"],
|
||||||
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
supported: {
|
supported: {
|
||||||
"top-level-await": true, //browsers can handle top-level-await features
|
"top-level-await": true, //browsers can handle top-level-await features
|
||||||
|
|||||||
275
yarn.lock
275
yarn.lock
@@ -683,25 +683,25 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@emnapi/core@^1.4.3", "@emnapi/core@^1.7.1", "@emnapi/core@^1.8.1":
|
"@emnapi/core@^1.4.3", "@emnapi/core@^1.8.1":
|
||||||
version "1.8.1"
|
version "1.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349"
|
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.2.tgz#3870265ecffc7352d01ead62d8d83d8358a2d034"
|
||||||
integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==
|
integrity sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emnapi/wasi-threads" "1.1.0"
|
"@emnapi/wasi-threads" "1.2.1"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.7.1", "@emnapi/runtime@^1.8.1":
|
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.8.1":
|
||||||
version "1.8.1"
|
version "1.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5"
|
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.2.tgz#8b469a3db160817cadb1de9050211a9d1ea84fa2"
|
||||||
integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==
|
integrity sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0":
|
"@emnapi/wasi-threads@1.2.1", "@emnapi/wasi-threads@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548"
|
||||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
@@ -2397,12 +2397,10 @@
|
|||||||
"@tybys/wasm-util" "^0.10.0"
|
"@tybys/wasm-util" "^0.10.0"
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime@^1.1.1":
|
"@napi-rs/wasm-runtime@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz#c3705ab549d176b8dc5172723d6156c3dc426af2"
|
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz#e25454b4d44cfabd21d1bc801705359870e33ecc"
|
||||||
integrity sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==
|
integrity sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emnapi/core" "^1.7.1"
|
|
||||||
"@emnapi/runtime" "^1.7.1"
|
|
||||||
"@tybys/wasm-util" "^0.10.1"
|
"@tybys/wasm-util" "^0.10.1"
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
@@ -2530,6 +2528,135 @@
|
|||||||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
||||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||||
|
|
||||||
|
"@radix-ui/primitive@1.1.3":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba"
|
||||||
|
integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==
|
||||||
|
|
||||||
|
"@radix-ui/react-compose-refs@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
|
||||||
|
integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==
|
||||||
|
|
||||||
|
"@radix-ui/react-context@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
|
||||||
|
integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog@^1.1.1":
|
||||||
|
version "1.1.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632"
|
||||||
|
integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.3"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
"@radix-ui/react-context" "1.1.2"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.1.11"
|
||||||
|
"@radix-ui/react-focus-guards" "1.1.3"
|
||||||
|
"@radix-ui/react-focus-scope" "1.1.7"
|
||||||
|
"@radix-ui/react-id" "1.1.1"
|
||||||
|
"@radix-ui/react-portal" "1.1.9"
|
||||||
|
"@radix-ui/react-presence" "1.1.5"
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-slot" "1.2.3"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||||
|
aria-hidden "^1.2.4"
|
||||||
|
react-remove-scroll "^2.6.3"
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer@1.1.11":
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37"
|
||||||
|
integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.3"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||||
|
"@radix-ui/react-use-escape-keydown" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-guards@1.1.3":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f"
|
||||||
|
integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope@1.1.7":
|
||||||
|
version "1.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d"
|
||||||
|
integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-id@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7"
|
||||||
|
integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-portal@1.1.9":
|
||||||
|
version "1.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472"
|
||||||
|
integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.1.3"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-presence@1.1.5":
|
||||||
|
version "1.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db"
|
||||||
|
integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@2.1.3":
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc"
|
||||||
|
integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-slot" "1.2.3"
|
||||||
|
|
||||||
|
"@radix-ui/react-slot@1.2.3":
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1"
|
||||||
|
integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.2"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-callback-ref@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40"
|
||||||
|
integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==
|
||||||
|
|
||||||
|
"@radix-ui/react-use-controllable-state@1.2.2":
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190"
|
||||||
|
integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-use-effect-event" "0.0.2"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-effect-event@0.0.2":
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907"
|
||||||
|
integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-escape-keydown@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29"
|
||||||
|
integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-layout-effect@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e"
|
||||||
|
integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==
|
||||||
|
|
||||||
"@repeaterjs/repeater@^3.0.4", "@repeaterjs/repeater@^3.0.6":
|
"@repeaterjs/repeater@^3.0.4", "@repeaterjs/repeater@^3.0.6":
|
||||||
version "3.0.6"
|
version "3.0.6"
|
||||||
resolved "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz"
|
resolved "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz"
|
||||||
@@ -3380,13 +3507,20 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz"
|
||||||
integrity sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==
|
integrity sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^25.3.0":
|
"@types/node@*":
|
||||||
version "25.3.0"
|
version "25.3.0"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz"
|
||||||
integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==
|
integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.18.0"
|
undici-types "~7.18.0"
|
||||||
|
|
||||||
|
"@types/node@^25.5.2":
|
||||||
|
version "25.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.5.2.tgz#94861e32f9ffd8de10b52bbec403465c84fff762"
|
||||||
|
integrity sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~7.18.0"
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz"
|
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz"
|
||||||
@@ -3846,6 +3980,13 @@ argparse@^2.0.1:
|
|||||||
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
|
aria-hidden@^1.2.4:
|
||||||
|
version "1.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a"
|
||||||
|
integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
aria-query@5.1.3:
|
aria-query@5.1.3:
|
||||||
version "5.1.3"
|
version "5.1.3"
|
||||||
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz"
|
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz"
|
||||||
@@ -5038,6 +5179,11 @@ detect-newline@^3.1.0:
|
|||||||
resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz"
|
||||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||||
|
|
||||||
|
detect-node-es@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
|
||||||
|
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
|
||||||
@@ -6127,6 +6273,15 @@ fraction.js@^5.3.4:
|
|||||||
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz"
|
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz"
|
||||||
integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==
|
integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==
|
||||||
|
|
||||||
|
framer-motion@^12.38.0:
|
||||||
|
version "12.38.0"
|
||||||
|
resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz"
|
||||||
|
integrity sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==
|
||||||
|
dependencies:
|
||||||
|
motion-dom "^12.38.0"
|
||||||
|
motion-utils "^12.36.0"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
fs-extra@^10.0.1:
|
fs-extra@^10.0.1:
|
||||||
version "10.1.0"
|
version "10.1.0"
|
||||||
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz"
|
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz"
|
||||||
@@ -6204,6 +6359,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4, get-intrinsic@
|
|||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
math-intrinsics "^1.1.0"
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
|
get-nonce@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
|
||||||
|
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
|
||||||
|
|
||||||
get-package-type@^0.1.0:
|
get-package-type@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz"
|
||||||
@@ -8096,6 +8256,26 @@ modern-tar@^0.7.3:
|
|||||||
resolved "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.5.tgz"
|
resolved "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.5.tgz"
|
||||||
integrity sha512-YTefgdpKKFgoTDbEUqXqgUJct2OG6/4hs4XWLsxcHkDLj/x/V8WmKIRppPnXP5feQ7d1vuYWSp3qKkxfwaFaxA==
|
integrity sha512-YTefgdpKKFgoTDbEUqXqgUJct2OG6/4hs4XWLsxcHkDLj/x/V8WmKIRppPnXP5feQ7d1vuYWSp3qKkxfwaFaxA==
|
||||||
|
|
||||||
|
motion-dom@^12.38.0:
|
||||||
|
version "12.38.0"
|
||||||
|
resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz"
|
||||||
|
integrity sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==
|
||||||
|
dependencies:
|
||||||
|
motion-utils "^12.36.0"
|
||||||
|
|
||||||
|
motion-utils@^12.36.0:
|
||||||
|
version "12.36.0"
|
||||||
|
resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz"
|
||||||
|
integrity sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==
|
||||||
|
|
||||||
|
motion@^12.38.0:
|
||||||
|
version "12.38.0"
|
||||||
|
resolved "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz"
|
||||||
|
integrity sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==
|
||||||
|
dependencies:
|
||||||
|
framer-motion "^12.38.0"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
|
||||||
@@ -8882,6 +9062,25 @@ react-refresh@^0.18.0:
|
|||||||
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz"
|
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz"
|
||||||
integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==
|
integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==
|
||||||
|
|
||||||
|
react-remove-scroll-bar@^2.3.7:
|
||||||
|
version "2.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
|
||||||
|
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
|
||||||
|
dependencies:
|
||||||
|
react-style-singleton "^2.2.2"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
react-remove-scroll@^2.6.3:
|
||||||
|
version "2.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz#6442da56791117661978ae99cd29be9026fecca0"
|
||||||
|
integrity sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==
|
||||||
|
dependencies:
|
||||||
|
react-remove-scroll-bar "^2.3.7"
|
||||||
|
react-style-singleton "^2.2.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
use-callback-ref "^1.3.3"
|
||||||
|
use-sidecar "^1.1.3"
|
||||||
|
|
||||||
react-router-dom@^7.13.1:
|
react-router-dom@^7.13.1:
|
||||||
version "7.13.1"
|
version "7.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz"
|
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz"
|
||||||
@@ -8932,6 +9131,14 @@ react-sliding-pane@^7.3.0:
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-modal "^3.14.3"
|
react-modal "^3.14.3"
|
||||||
|
|
||||||
|
react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
|
||||||
|
version "2.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
|
||||||
|
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
|
||||||
|
dependencies:
|
||||||
|
get-nonce "^1.0.0"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
react-swipeable@^7.0.2:
|
react-swipeable@^7.0.2:
|
||||||
version "7.0.2"
|
version "7.0.2"
|
||||||
resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz"
|
resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz"
|
||||||
@@ -10134,10 +10341,10 @@ typescript@^4.3.2:
|
|||||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
|
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
|
||||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||||
|
|
||||||
typescript@^5.9.3:
|
typescript@^6.0.2:
|
||||||
version "5.9.3"
|
version "6.0.2"
|
||||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.2.tgz#0b1bfb15f68c64b97032f3d78abbf98bdbba501f"
|
||||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
integrity sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==
|
||||||
|
|
||||||
ua-parser-js@^1.0.35:
|
ua-parser-js@^1.0.35:
|
||||||
version "1.0.41"
|
version "1.0.41"
|
||||||
@@ -10265,6 +10472,13 @@ urlpattern-polyfill@^10.0.0:
|
|||||||
resolved "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz"
|
resolved "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz"
|
||||||
integrity sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==
|
integrity sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==
|
||||||
|
|
||||||
|
use-callback-ref@^1.3.3:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
|
||||||
|
integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
use-composed-ref@^1.3.0:
|
use-composed-ref@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz"
|
resolved "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz"
|
||||||
@@ -10287,6 +10501,14 @@ use-latest@^1.2.1, use-latest@^1.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
|
use-sidecar@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
|
||||||
|
integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
|
||||||
|
dependencies:
|
||||||
|
detect-node-es "^1.1.0"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
use-sync-external-store@^1.6.0:
|
use-sync-external-store@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
||||||
@@ -10336,6 +10558,13 @@ v8-to-istanbul@^9.0.1:
|
|||||||
"@types/istanbul-lib-coverage" "^2.0.1"
|
"@types/istanbul-lib-coverage" "^2.0.1"
|
||||||
convert-source-map "^2.0.0"
|
convert-source-map "^2.0.0"
|
||||||
|
|
||||||
|
vaul@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.2.tgz#c959f8b9dc2ed4f7d99366caee433fbef91f5ba9"
|
||||||
|
integrity sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-dialog" "^1.1.1"
|
||||||
|
|
||||||
vite-plugin-html@^3.2.2:
|
vite-plugin-html@^3.2.2:
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz"
|
resolved "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user