jashdoshi77 commited on
Commit
ba95018
Β·
0 Parent(s):

OT NoteBuilder - Production deployment

Browse files
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ .venv
3
+ venv
4
+ __pycache__
5
+ *.pyc
6
+ .env
7
+ .env.*
8
+ *.docx
9
+ .git
10
+ frontend/node_modules
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment secrets
2
+ .env
3
+ .env.*
4
+
5
+ # Source template document (confidential)
6
+ *.docx
7
+ doc_content.txt
8
+ convert_template.cjs
9
+ template_data.json
10
+
11
+ # Python
12
+ __pycache__/
13
+ *.pyc
14
+ *.pyo
15
+ venv/
16
+ .venv/
17
+
18
+ # Node
19
+ node_modules/
20
+ dist/
21
+ .vite/
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Stage 1: Build React frontend ──
2
+ FROM node:20-slim AS frontend-build
3
+
4
+ WORKDIR /app/frontend
5
+ COPY frontend/package.json frontend/package-lock.json* ./
6
+ RUN npm ci --production=false
7
+ COPY frontend/ ./
8
+ RUN npm run build
9
+
10
+ # ── Stage 2: Production image ──
11
+ FROM python:3.11-slim
12
+
13
+ WORKDIR /app
14
+
15
+ # Install Python dependencies
16
+ COPY backend/requirements.txt ./
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy backend code
20
+ COPY backend/ ./
21
+
22
+ # Copy built frontend into backend's static directory
23
+ COPY --from=frontend-build /app/frontend/dist ./static
24
+
25
+ # HuggingFace Spaces requires port 7860
26
+ EXPOSE 7860
27
+
28
+ # Run with uvicorn (production)
29
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: OT NoteBuilder
3
+ emoji: πŸ“‹
4
+ colorFrom: green
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
backend/main.py ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SNF OT Daily Progress Note Generator β€” FastAPI Backend
3
+ Handles Groq AI integration for converting structured selections into
4
+ Med A-compliant clinical paragraphs.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import json
10
+ import logging
11
+ import hashlib
12
+ import hmac
13
+ import time
14
+ import secrets
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from fastapi import FastAPI, HTTPException, Request, Depends
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
20
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
21
+ from pydantic import BaseModel, Field, validator
22
+ from dotenv import load_dotenv
23
+ from groq import Groq
24
+ import httpx
25
+ from slowapi import Limiter
26
+ from slowapi.util import get_remote_address
27
+ from slowapi.errors import RateLimitExceeded
28
+ from fastapi.responses import JSONResponse
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Config
32
+ # ---------------------------------------------------------------------------
33
+
34
+ load_dotenv()
35
+
36
+ logging.basicConfig(level=logging.INFO)
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # Groq API keys β€” multiple for fallback on rate limits
40
+ _raw_keys = os.getenv("GROQ_API_KEYS", os.getenv("GROQ_API_KEY", ""))
41
+ GROQ_API_KEYS = [k.strip() for k in _raw_keys.split(",") if k.strip()]
42
+ ALLOWED_ORIGINS = os.getenv(
43
+ "ALLOWED_ORIGINS",
44
+ "http://localhost:5173,http://localhost:3000,https://jashdoshi77-ot.hf.space"
45
+ ).split(",")
46
+ GROQ_MODEL = "llama-3.3-70b-versatile"
47
+
48
+ # JWT secret
49
+ JWT_SECRET = os.getenv("JWT_SECRET", secrets.token_hex(32))
50
+ JWT_EXPIRY_HOURS = 24
51
+
52
+ logger.info(f"Loaded {len(GROQ_API_KEYS)} Groq API key(s)")
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Authorized users β€” loaded from env (format: user1:pass1,user2:pass2)
56
+ # ---------------------------------------------------------------------------
57
+
58
+ def _hash_pw(pw: str) -> str:
59
+ return hashlib.sha256(pw.encode()).hexdigest()
60
+
61
+ def _load_users() -> dict:
62
+ raw = os.getenv("AUTH_USERS", "")
63
+ users = {}
64
+ for pair in raw.split(","):
65
+ pair = pair.strip()
66
+ if ":" in pair:
67
+ uname, pw = pair.split(":", 1)
68
+ users[uname.strip().lower()] = _hash_pw(pw.strip())
69
+ return users
70
+
71
+ AUTHORIZED_USERS = _load_users()
72
+ logger.info(f"Loaded {len(AUTHORIZED_USERS)} authorized user(s)")
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # JWT helpers (lightweight, no external dependency)
76
+ # ---------------------------------------------------------------------------
77
+
78
+ import base64, json as _json
79
+
80
+ def _b64url_encode(data: bytes) -> str:
81
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
82
+
83
+ def _b64url_decode(s: str) -> bytes:
84
+ s += "=" * (4 - len(s) % 4)
85
+ return base64.urlsafe_b64decode(s)
86
+
87
+ def create_jwt(username: str) -> str:
88
+ header = _b64url_encode(_json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
89
+ payload = _b64url_encode(_json.dumps({
90
+ "sub": username,
91
+ "iat": int(time.time()),
92
+ "exp": int(time.time()) + JWT_EXPIRY_HOURS * 3600,
93
+ }).encode())
94
+ signature = hmac.new(JWT_SECRET.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest()
95
+ sig_b64 = _b64url_encode(signature)
96
+ return f"{header}.{payload}.{sig_b64}"
97
+
98
+ def verify_jwt(token: str) -> dict:
99
+ try:
100
+ parts = token.split(".")
101
+ if len(parts) != 3:
102
+ raise ValueError("Invalid token")
103
+ header_b64, payload_b64, sig_b64 = parts
104
+ expected_sig = hmac.new(JWT_SECRET.encode(), f"{header_b64}.{payload_b64}".encode(), hashlib.sha256).digest()
105
+ actual_sig = _b64url_decode(sig_b64)
106
+ if not hmac.compare_digest(expected_sig, actual_sig):
107
+ raise ValueError("Invalid signature")
108
+ payload = _json.loads(_b64url_decode(payload_b64))
109
+ if payload.get("exp", 0) < time.time():
110
+ raise ValueError("Token expired")
111
+ return payload
112
+ except Exception as e:
113
+ raise ValueError(f"Token verification failed: {e}")
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Auth dependency
117
+ # ---------------------------------------------------------------------------
118
+
119
+ bearer_scheme = HTTPBearer()
120
+
121
+ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
122
+ try:
123
+ payload = verify_jwt(credentials.credentials)
124
+ username = payload.get("sub")
125
+ if username not in AUTHORIZED_USERS:
126
+ raise HTTPException(status_code=401, detail="User not authorized")
127
+ return username
128
+ except ValueError as e:
129
+ raise HTTPException(status_code=401, detail=str(e))
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Rate limiting
133
+ # ---------------------------------------------------------------------------
134
+
135
+ limiter = Limiter(key_func=get_remote_address)
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # FastAPI app
139
+ # ---------------------------------------------------------------------------
140
+
141
+ app = FastAPI(
142
+ title="OT Note Generator API",
143
+ version="1.0.0",
144
+ docs_url="/api/docs",
145
+ redoc_url=None,
146
+ )
147
+
148
+ app.state.limiter = limiter
149
+
150
+
151
+ @app.exception_handler(RateLimitExceeded)
152
+ async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
153
+ return JSONResponse(
154
+ status_code=429,
155
+ content={"detail": "Rate limit exceeded. Please wait before trying again."},
156
+ )
157
+
158
+
159
+ # Security middleware
160
+ app.add_middleware(
161
+ CORSMiddleware,
162
+ allow_origins=ALLOWED_ORIGINS,
163
+ allow_credentials=True,
164
+ allow_methods=["GET", "POST"],
165
+ allow_headers=["Content-Type", "Authorization"],
166
+ max_age=600,
167
+ )
168
+
169
+ app.add_middleware(
170
+ TrustedHostMiddleware,
171
+ allowed_hosts=["localhost", "127.0.0.1", "*.localhost", "*.hf.space", "*.huggingface.co"],
172
+ )
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Pydantic models
176
+ # ---------------------------------------------------------------------------
177
+
178
+ MAX_SECTIONS = 20
179
+ MAX_FIELDS_PER_SECTION = 200
180
+ MAX_TEXT_LENGTH = 2000
181
+
182
+
183
+ class FieldSelection(BaseModel):
184
+ fieldId: str = Field(..., max_length=200)
185
+ fieldLabel: str = Field(..., max_length=500)
186
+ selectedOptions: List[str] = Field(default_factory=list)
187
+ customText: Optional[str] = Field(None, max_length=MAX_TEXT_LENGTH)
188
+ numericValue: Optional[str] = Field(None, max_length=50)
189
+
190
+ @validator("selectedOptions", each_item=True)
191
+ def validate_option_length(cls, v):
192
+ if len(v) > 500:
193
+ raise ValueError("Option text too long")
194
+ return v
195
+
196
+
197
+ class SectionData(BaseModel):
198
+ sectionId: str = Field(..., max_length=200)
199
+ sectionTitle: str = Field(..., max_length=300)
200
+ enabled: bool = True
201
+ fields: List[FieldSelection] = Field(default_factory=list)
202
+
203
+ @validator("fields")
204
+ def validate_fields_count(cls, v):
205
+ if len(v) > MAX_FIELDS_PER_SECTION:
206
+ raise ValueError(f"Too many fields (max {MAX_FIELDS_PER_SECTION})")
207
+ return v
208
+
209
+
210
+ class GenerateRequest(BaseModel):
211
+ patientInfo: Dict[str, str] = Field(default_factory=dict)
212
+ sections: List[SectionData] = Field(default_factory=list)
213
+ customSections: List[SectionData] = Field(default_factory=list)
214
+
215
+ @validator("sections")
216
+ def validate_sections_count(cls, v):
217
+ if len(v) > MAX_SECTIONS:
218
+ raise ValueError(f"Too many sections (max {MAX_SECTIONS})")
219
+ return v
220
+
221
+ @validator("patientInfo")
222
+ def sanitize_patient_info(cls, v):
223
+ sanitized = {}
224
+ for key, value in v.items():
225
+ clean_key = re.sub(r"[^\w\s\-/]", "", key)[:100]
226
+ clean_val = re.sub(r"[<>]", "", str(value))[:500]
227
+ sanitized[clean_key] = clean_val
228
+ return sanitized
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # System prompt for Groq
233
+ # ---------------------------------------------------------------------------
234
+
235
+ SYSTEM_PROMPT = """You are an expert Occupational Therapy clinical documentation specialist. Your job is to convert structured selections from an OT daily progress note template into polished, professional, Med A-compliant clinical narrative paragraphs.
236
+
237
+ RULES:
238
+ 1. Write in the EXACT style of skilled nursing facility (SNF) occupational therapy daily treatment encounter notes.
239
+ 2. Use proper OT abbreviations: pt (patient), BUE/BLE (bilateral upper/lower extremity), AROM (active ROM), ADL, IADL, LBD/UBD (lower/upper body dressing), FM (fine motor), GM (gross motor), vc (verbal cue), tc (tactile cue), hoh (hand-over-hand), SBA (standby assist), CGA (contact guard assist), Min/Mod/Max A (assist levels), ec (energy conservation), JP (joint protection), STS (sit to stand), w/c (wheelchair), EOB (edge of bed), WFL (within functional limits), 2 degrees (secondary to), d/c (discharge), HEP (home exercise program), etc.
240
+ 3. IMPORTANT: You MUST write a SEPARATE section with its own header for EVERY section provided in the input. This includes non-CPT sections like Patient Presentation, Education Provided, Response to Treatment, Assessment, Therapist Signature, and any other section. Do NOT merge or absorb non-CPT sections into CPT sections.
241
+ 4. DO NOT use asterisks (*), stars, bold markers, or any markdown formatting whatsoever. Output must be clean plain text only.
242
+ 5. DO NOT use em dashes, en dashes, or any special unicode characters. Use regular hyphens (-) or commas instead.
243
+ 6. Write flowing narrative paragraphs, NOT bullet points.
244
+ 7. Include clinical justification language that supports medical necessity.
245
+ 8. Maintain a professional, skilled, clinical tone throughout.
246
+ 9. If exercise details (sets/reps) are provided, weave them naturally into sentences.
247
+ 10. Connect deficits to functional impacts and goals.
248
+ 11. Include cueing types and assist levels naturally in the narrative.
249
+ 12. Do NOT invent information not provided in the selections.
250
+ 13. If a section has no selections, skip it entirely.
251
+ 14. Format each section header as plain text followed by a colon. For CPT sections include the code, e.g. "Therapeutic Exercise (97110):". For non-CPT sections just use the name, e.g. "Patient Presentation:" or "Assessment:".
252
+ 15. Use regular quotation marks and standard ASCII punctuation only.
253
+ 16. The order of sections in the output should match the order they appear in the input.
254
+
255
+ EXAMPLE OUTPUT STYLE:
256
+
257
+ Patient Presentation:
258
+ Pt was seen seated in w/c upon arrival, presenting with an initially reluctant demeanor; however, with skilled encouragement and verbal cueing, pt became engaged and receptive to treatment. Pt c/o pain at 6/10 localized to the right shoulder, which was managed with positioning and activity modification. Pt was alert and oriented x3 (person, place, time).
259
+
260
+ Therapeutic Exercise (97110):
261
+ Pt participated in skilled therapeutic exercise targeting BUE strength and AROM in seated position with back support to improve functional performance with ADLs, transfers, and mobility. Exercises were graded by therapist to address grip strength and core stability that directly impact pt's safe and independent functional task performance. Exercises performed today included shoulder flexion (3 x 10), shoulder abduction (3 x 10), and grip strengthening via putty squeeze. Pt required vc/tc for proper joint alignment and controlled movement speed throughout exercise program. Pt demonstrated fair tolerance to therapeutic exercise, completing 20 min of activity with intermittent rest breaks (1-2) secondary to fatigue and decreased endurance.
262
+
263
+ Therapeutic Activities (97530):
264
+ Pt engaged in skilled therapeutic activities addressing static and dynamic standing balance in alignment with established OT goals and POC. Pt participated in dynamic standing ball toss to targets and ring toss on cone (3 x 10 each) to improve dynamic balance and postural reactions. Pt required CGA for balance and safety, as well as vc for safety awareness and fall prevention. Pt demonstrated fair tolerance to therapeutic activities, with performance impacted by decreased endurance and impaired postural control.
265
+
266
+ Self-Care / ADL Training (97535):
267
+ Pt engaged in skilled ADL retraining focused on UBD and LBD and toileting to support goal of increased functional independence. Pt performed LBD donning with Min A, doffing with SBA. AE utilized: reacher and sock aid. Pt was able to thread bilateral LE through garment independently and pull clothing to knee level.
268
+
269
+ Education Provided:
270
+ Pt and/or caregiver educated on ec strategies for ADL participation, safe transfer techniques, and HEP compliance. Pt demonstrated fair understanding via return demonstration with minimal cueing required.
271
+
272
+ Response to Treatment:
273
+ Pt demonstrated fair participation with fatigue and decreased endurance, requiring frequent rest breaks and cueing for task completion, but was able to complete all graded activities with assistance. Pt demonstrated emerging carryover of compensatory strategies with minimal verbal cueing.
274
+
275
+ Assessment:
276
+ Pt presents with deficits in bed mobility, strength, activity tolerance, and ADL sequencing, impacting independence in self-care tasks. Skilled OT remains necessary for functional mobility training, ADL retraining, balance, and fine motor coordination to improve safety and independence.
277
+ """
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Helper: build user message from selections
281
+ # ---------------------------------------------------------------------------
282
+
283
+
284
+ def build_user_message(data: GenerateRequest) -> str:
285
+ parts: List[str] = []
286
+
287
+ # Patient info
288
+ if data.patientInfo:
289
+ info_lines = [f"- {k}: {v}" for k, v in data.patientInfo.items() if v]
290
+ if info_lines:
291
+ parts.append("PATIENT INFO:\n" + "\n".join(info_lines))
292
+
293
+ # Sections
294
+ all_sections = [s for s in data.sections if s.enabled] + [
295
+ s for s in data.customSections if s.enabled
296
+ ]
297
+
298
+ for section in all_sections:
299
+ has_data = any(
300
+ f.selectedOptions or f.customText or f.numericValue for f in section.fields
301
+ )
302
+ if not has_data:
303
+ continue
304
+
305
+ section_lines = [f"\n--- {section.sectionTitle} ---"]
306
+
307
+ for field in section.fields:
308
+ if not field.selectedOptions and not field.customText and not field.numericValue:
309
+ continue
310
+
311
+ line = f" {field.fieldLabel}: "
312
+ values = []
313
+
314
+ if field.selectedOptions:
315
+ values.extend(field.selectedOptions)
316
+ if field.customText:
317
+ values.append(field.customText)
318
+ if field.numericValue:
319
+ values.append(field.numericValue)
320
+
321
+ line += " / ".join(values)
322
+ section_lines.append(line)
323
+
324
+ parts.append("\n".join(section_lines))
325
+
326
+ return "\n".join(parts)
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # Routes
331
+ # ---------------------------------------------------------------------------
332
+
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # Auth models & login route
336
+ # ---------------------------------------------------------------------------
337
+
338
+ class LoginRequest(BaseModel):
339
+ username: str = Field(..., max_length=100)
340
+ password: str = Field(..., max_length=200)
341
+
342
+
343
+ @app.post("/api/login")
344
+ @limiter.limit("5/minute")
345
+ async def login(request: Request, data: LoginRequest):
346
+ username = data.username.strip().lower()
347
+ password_hash = _hash_pw(data.password)
348
+
349
+ stored_hash = AUTHORIZED_USERS.get(username)
350
+ if not stored_hash or not hmac.compare_digest(stored_hash, password_hash):
351
+ logger.warning(f"Failed login attempt for user: {username}")
352
+ raise HTTPException(status_code=401, detail="Invalid username or password")
353
+
354
+ token = create_jwt(username)
355
+ logger.info(f"User '{username}' logged in successfully")
356
+ return {"token": token, "username": username}
357
+
358
+
359
+ @app.get("/api/verify")
360
+ async def verify_token(user: str = Depends(get_current_user)):
361
+ return {"valid": True, "username": user}
362
+
363
+
364
+ # Load template data β€” encrypted at rest, decrypted at startup using TEMPLATE_KEY
365
+ _TEMPLATE_DATA = None
366
+ _base_dir = os.path.dirname(__file__)
367
+ _enc_path = os.path.join(_base_dir, "template_data.enc")
368
+ _json_path = os.path.join(_base_dir, "template_data.json")
369
+
370
+ try:
371
+ if os.path.exists(_enc_path):
372
+ # Production: decrypt the encrypted template
373
+ from cryptography.fernet import Fernet
374
+ _template_key = os.getenv("TEMPLATE_KEY")
375
+ if not _template_key:
376
+ raise RuntimeError("TEMPLATE_KEY env variable is required to decrypt template data")
377
+ _fernet = Fernet(_template_key.encode())
378
+ with open(_enc_path, "rb") as f:
379
+ _encrypted = f.read()
380
+ _decrypted = _fernet.decrypt(_encrypted)
381
+ _TEMPLATE_DATA = json.loads(_decrypted.decode("utf-8"))
382
+ logger.info(f"Loaded encrypted template data ({len(_decrypted) // 1024} KB)")
383
+ elif os.path.exists(_json_path):
384
+ # Local dev fallback: load plain JSON
385
+ with open(_json_path, "r", encoding="utf-8") as f:
386
+ _TEMPLATE_DATA = json.load(f)
387
+ logger.info(f"Loaded template data from JSON ({os.path.getsize(_json_path) // 1024} KB)")
388
+ else:
389
+ logger.warning("No template data file found β€” /api/template will be unavailable")
390
+ except Exception as e:
391
+ logger.error(f"Failed to load template data: {e}")
392
+ _TEMPLATE_DATA = None
393
+
394
+
395
+ @app.get("/api/template")
396
+ async def get_template(user: str = Depends(get_current_user)):
397
+ """Serves the template structure. Requires authentication."""
398
+ if _TEMPLATE_DATA is None:
399
+ raise HTTPException(status_code=500, detail="Template data not available")
400
+ return _TEMPLATE_DATA
401
+
402
+
403
+ @app.get("/api/health")
404
+ async def health_check():
405
+ return {"status": "ok", "model": GROQ_MODEL}
406
+
407
+
408
+ @app.post("/api/generate")
409
+ @limiter.limit("10/minute")
410
+ async def generate_note(request: Request, data: GenerateRequest, user: str = Depends(get_current_user)):
411
+ if not GROQ_API_KEYS:
412
+ raise HTTPException(
413
+ status_code=500,
414
+ detail="No Groq API keys configured. Please set GROQ_API_KEYS in .env file.",
415
+ )
416
+
417
+ user_message = build_user_message(data)
418
+
419
+ if not user_message.strip():
420
+ raise HTTPException(
421
+ status_code=400,
422
+ detail="No selections provided. Please fill in at least one section.",
423
+ )
424
+
425
+ logger.info(f"Generating note for '{user}' - payload size: {len(user_message)} chars")
426
+
427
+ messages = [
428
+ {"role": "system", "content": SYSTEM_PROMPT},
429
+ {
430
+ "role": "user",
431
+ "content": f"Convert the following OT daily note selections into a polished, professional Med A-compliant clinical narrative:\n\n{user_message}",
432
+ },
433
+ ]
434
+
435
+ last_error = None
436
+
437
+ # Try each API key in order β€” rotate on rate limit or auth errors
438
+ for i, api_key in enumerate(GROQ_API_KEYS):
439
+ try:
440
+ key_label = f"key #{i + 1}/{len(GROQ_API_KEYS)}"
441
+ logger.info(f"Attempting generation with {key_label}")
442
+
443
+ http_client = httpx.Client()
444
+ client = Groq(api_key=api_key, http_client=http_client)
445
+ chat_completion = client.chat.completions.create(
446
+ messages=messages,
447
+ model=GROQ_MODEL,
448
+ temperature=0.7,
449
+ max_tokens=4096,
450
+ top_p=0.9,
451
+ )
452
+
453
+ generated_text = chat_completion.choices[0].message.content
454
+ logger.info(f"Generation succeeded with {key_label}")
455
+
456
+ return {
457
+ "success": True,
458
+ "note": generated_text,
459
+ "model": GROQ_MODEL,
460
+ "usage": {
461
+ "prompt_tokens": chat_completion.usage.prompt_tokens,
462
+ "completion_tokens": chat_completion.usage.completion_tokens,
463
+ },
464
+ }
465
+
466
+ except Exception as e:
467
+ last_error = str(e)
468
+ error_lower = last_error.lower()
469
+ # If it's a rate limit or auth error, try the next key
470
+ if "rate_limit" in error_lower or "429" in error_lower or "quota" in error_lower or "limit" in error_lower:
471
+ logger.warning(f"{key_label} hit rate limit, trying next key...")
472
+ continue
473
+ elif "401" in error_lower or "invalid" in error_lower or "auth" in error_lower:
474
+ logger.warning(f"{key_label} auth error, trying next key...")
475
+ continue
476
+ else:
477
+ # Non-recoverable error β€” don't try other keys
478
+ logger.error(f"Groq API error (non-recoverable): {last_error}")
479
+ raise HTTPException(status_code=502, detail=f"AI generation failed: {last_error}")
480
+
481
+ # All keys exhausted
482
+ logger.error(f"All {len(GROQ_API_KEYS)} API keys exhausted. Last error: {last_error}")
483
+ raise HTTPException(
484
+ status_code=502,
485
+ detail="All API keys have been rate-limited. Please wait a moment and try again.",
486
+ )
487
+
488
+
489
+ # ---------------------------------------------------------------------------
490
+ # Static file serving (production β€” serves built React app)
491
+ # ---------------------------------------------------------------------------
492
+
493
+ from fastapi.staticfiles import StaticFiles
494
+ from fastapi.responses import FileResponse
495
+
496
+ _static_dir = os.path.join(os.path.dirname(__file__), "static")
497
+
498
+ if os.path.isdir(_static_dir):
499
+ # Serve static assets (JS, CSS, images)
500
+ app.mount("/assets", StaticFiles(directory=os.path.join(_static_dir, "assets")), name="assets")
501
+
502
+ # SPA fallback β€” serve index.html for all non-API routes
503
+ @app.get("/{path:path}")
504
+ async def serve_spa(path: str):
505
+ # If the file exists in static dir, serve it
506
+ file_path = os.path.join(_static_dir, path)
507
+ if os.path.isfile(file_path):
508
+ return FileResponse(file_path)
509
+ # Otherwise serve index.html (SPA routing)
510
+ return FileResponse(os.path.join(_static_dir, "index.html"))
511
+
512
+ logger.info(f"Serving frontend from {_static_dir}")
513
+ else:
514
+ logger.info("No static directory found β€” running in API-only mode")
515
+
516
+
517
+ # ---------------------------------------------------------------------------
518
+ # Entry point
519
+ # ---------------------------------------------------------------------------
520
+
521
+ if __name__ == "__main__":
522
+ import uvicorn
523
+
524
+ port = int(os.getenv("PORT", "7860"))
525
+ uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
backend/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.0
3
+ python-dotenv==1.0.1
4
+ groq==0.9.0
5
+ pydantic==2.9.0
6
+ python-multipart==0.0.9
7
+ slowapi==0.1.9
8
+ httpx==0.27.0
9
+ cryptography==42.0.0
backend/template_data.enc ADDED
The diff for this file is too large to render. See raw diff
 
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="SNF Occupational Therapy Daily Progress Note Builder - AI-powered clinical documentation tool" />
7
+ <meta name="theme-color" content="#10b981" />
8
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'/%3E%3C/svg%3E" />
9
+ <title>OT NoteBuilder β€” Clinical Documentation Tool</title>
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.jsx"></script>
14
+ </body>
15
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "frontend",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "lucide-react": "^1.8.0",
12
+ "react": "^19.2.4",
13
+ "react-dom": "^19.2.4",
14
+ "react-hot-toast": "^2.6.0"
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/js": "^9.39.4",
18
+ "@types/react": "^19.2.14",
19
+ "@types/react-dom": "^19.2.3",
20
+ "@vitejs/plugin-react": "^6.0.1",
21
+ "eslint": "^9.39.4",
22
+ "eslint-plugin-react-hooks": "^7.0.1",
23
+ "eslint-plugin-react-refresh": "^0.5.2",
24
+ "globals": "^17.4.0",
25
+ "vite": "^8.0.4"
26
+ }
27
+ },
28
+ "node_modules/@babel/code-frame": {
29
+ "version": "7.29.0",
30
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
31
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@babel/helper-validator-identifier": "^7.28.5",
36
+ "js-tokens": "^4.0.0",
37
+ "picocolors": "^1.1.1"
38
+ },
39
+ "engines": {
40
+ "node": ">=6.9.0"
41
+ }
42
+ },
43
+ "node_modules/@babel/compat-data": {
44
+ "version": "7.29.0",
45
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
46
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
47
+ "dev": true,
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=6.9.0"
51
+ }
52
+ },
53
+ "node_modules/@babel/core": {
54
+ "version": "7.29.0",
55
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
56
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
57
+ "dev": true,
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "@babel/code-frame": "^7.29.0",
61
+ "@babel/generator": "^7.29.0",
62
+ "@babel/helper-compilation-targets": "^7.28.6",
63
+ "@babel/helper-module-transforms": "^7.28.6",
64
+ "@babel/helpers": "^7.28.6",
65
+ "@babel/parser": "^7.29.0",
66
+ "@babel/template": "^7.28.6",
67
+ "@babel/traverse": "^7.29.0",
68
+ "@babel/types": "^7.29.0",
69
+ "@jridgewell/remapping": "^2.3.5",
70
+ "convert-source-map": "^2.0.0",
71
+ "debug": "^4.1.0",
72
+ "gensync": "^1.0.0-beta.2",
73
+ "json5": "^2.2.3",
74
+ "semver": "^6.3.1"
75
+ },
76
+ "engines": {
77
+ "node": ">=6.9.0"
78
+ },
79
+ "funding": {
80
+ "type": "opencollective",
81
+ "url": "https://opencollective.com/babel"
82
+ }
83
+ },
84
+ "node_modules/@babel/generator": {
85
+ "version": "7.29.1",
86
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
87
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
88
+ "dev": true,
89
+ "license": "MIT",
90
+ "dependencies": {
91
+ "@babel/parser": "^7.29.0",
92
+ "@babel/types": "^7.29.0",
93
+ "@jridgewell/gen-mapping": "^0.3.12",
94
+ "@jridgewell/trace-mapping": "^0.3.28",
95
+ "jsesc": "^3.0.2"
96
+ },
97
+ "engines": {
98
+ "node": ">=6.9.0"
99
+ }
100
+ },
101
+ "node_modules/@babel/helper-compilation-targets": {
102
+ "version": "7.28.6",
103
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
104
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
105
+ "dev": true,
106
+ "license": "MIT",
107
+ "dependencies": {
108
+ "@babel/compat-data": "^7.28.6",
109
+ "@babel/helper-validator-option": "^7.27.1",
110
+ "browserslist": "^4.24.0",
111
+ "lru-cache": "^5.1.1",
112
+ "semver": "^6.3.1"
113
+ },
114
+ "engines": {
115
+ "node": ">=6.9.0"
116
+ }
117
+ },
118
+ "node_modules/@babel/helper-globals": {
119
+ "version": "7.28.0",
120
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
121
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
122
+ "dev": true,
123
+ "license": "MIT",
124
+ "engines": {
125
+ "node": ">=6.9.0"
126
+ }
127
+ },
128
+ "node_modules/@babel/helper-module-imports": {
129
+ "version": "7.28.6",
130
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
131
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
132
+ "dev": true,
133
+ "license": "MIT",
134
+ "dependencies": {
135
+ "@babel/traverse": "^7.28.6",
136
+ "@babel/types": "^7.28.6"
137
+ },
138
+ "engines": {
139
+ "node": ">=6.9.0"
140
+ }
141
+ },
142
+ "node_modules/@babel/helper-module-transforms": {
143
+ "version": "7.28.6",
144
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
145
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
146
+ "dev": true,
147
+ "license": "MIT",
148
+ "dependencies": {
149
+ "@babel/helper-module-imports": "^7.28.6",
150
+ "@babel/helper-validator-identifier": "^7.28.5",
151
+ "@babel/traverse": "^7.28.6"
152
+ },
153
+ "engines": {
154
+ "node": ">=6.9.0"
155
+ },
156
+ "peerDependencies": {
157
+ "@babel/core": "^7.0.0"
158
+ }
159
+ },
160
+ "node_modules/@babel/helper-string-parser": {
161
+ "version": "7.27.1",
162
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
163
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
164
+ "dev": true,
165
+ "license": "MIT",
166
+ "engines": {
167
+ "node": ">=6.9.0"
168
+ }
169
+ },
170
+ "node_modules/@babel/helper-validator-identifier": {
171
+ "version": "7.28.5",
172
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
173
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
174
+ "dev": true,
175
+ "license": "MIT",
176
+ "engines": {
177
+ "node": ">=6.9.0"
178
+ }
179
+ },
180
+ "node_modules/@babel/helper-validator-option": {
181
+ "version": "7.27.1",
182
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
183
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
184
+ "dev": true,
185
+ "license": "MIT",
186
+ "engines": {
187
+ "node": ">=6.9.0"
188
+ }
189
+ },
190
+ "node_modules/@babel/helpers": {
191
+ "version": "7.29.2",
192
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
193
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
194
+ "dev": true,
195
+ "license": "MIT",
196
+ "dependencies": {
197
+ "@babel/template": "^7.28.6",
198
+ "@babel/types": "^7.29.0"
199
+ },
200
+ "engines": {
201
+ "node": ">=6.9.0"
202
+ }
203
+ },
204
+ "node_modules/@babel/parser": {
205
+ "version": "7.29.2",
206
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
207
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
208
+ "dev": true,
209
+ "license": "MIT",
210
+ "dependencies": {
211
+ "@babel/types": "^7.29.0"
212
+ },
213
+ "bin": {
214
+ "parser": "bin/babel-parser.js"
215
+ },
216
+ "engines": {
217
+ "node": ">=6.0.0"
218
+ }
219
+ },
220
+ "node_modules/@babel/template": {
221
+ "version": "7.28.6",
222
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
223
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
224
+ "dev": true,
225
+ "license": "MIT",
226
+ "dependencies": {
227
+ "@babel/code-frame": "^7.28.6",
228
+ "@babel/parser": "^7.28.6",
229
+ "@babel/types": "^7.28.6"
230
+ },
231
+ "engines": {
232
+ "node": ">=6.9.0"
233
+ }
234
+ },
235
+ "node_modules/@babel/traverse": {
236
+ "version": "7.29.0",
237
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
238
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
239
+ "dev": true,
240
+ "license": "MIT",
241
+ "dependencies": {
242
+ "@babel/code-frame": "^7.29.0",
243
+ "@babel/generator": "^7.29.0",
244
+ "@babel/helper-globals": "^7.28.0",
245
+ "@babel/parser": "^7.29.0",
246
+ "@babel/template": "^7.28.6",
247
+ "@babel/types": "^7.29.0",
248
+ "debug": "^4.3.1"
249
+ },
250
+ "engines": {
251
+ "node": ">=6.9.0"
252
+ }
253
+ },
254
+ "node_modules/@babel/types": {
255
+ "version": "7.29.0",
256
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
257
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
258
+ "dev": true,
259
+ "license": "MIT",
260
+ "dependencies": {
261
+ "@babel/helper-string-parser": "^7.27.1",
262
+ "@babel/helper-validator-identifier": "^7.28.5"
263
+ },
264
+ "engines": {
265
+ "node": ">=6.9.0"
266
+ }
267
+ },
268
+ "node_modules/@emnapi/core": {
269
+ "version": "1.9.2",
270
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
271
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
272
+ "dev": true,
273
+ "license": "MIT",
274
+ "optional": true,
275
+ "dependencies": {
276
+ "@emnapi/wasi-threads": "1.2.1",
277
+ "tslib": "^2.4.0"
278
+ }
279
+ },
280
+ "node_modules/@emnapi/runtime": {
281
+ "version": "1.9.2",
282
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
283
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
284
+ "dev": true,
285
+ "license": "MIT",
286
+ "optional": true,
287
+ "dependencies": {
288
+ "tslib": "^2.4.0"
289
+ }
290
+ },
291
+ "node_modules/@emnapi/wasi-threads": {
292
+ "version": "1.2.1",
293
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
294
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
295
+ "dev": true,
296
+ "license": "MIT",
297
+ "optional": true,
298
+ "dependencies": {
299
+ "tslib": "^2.4.0"
300
+ }
301
+ },
302
+ "node_modules/@eslint-community/eslint-utils": {
303
+ "version": "4.9.1",
304
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
305
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
306
+ "dev": true,
307
+ "license": "MIT",
308
+ "dependencies": {
309
+ "eslint-visitor-keys": "^3.4.3"
310
+ },
311
+ "engines": {
312
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
313
+ },
314
+ "funding": {
315
+ "url": "https://opencollective.com/eslint"
316
+ },
317
+ "peerDependencies": {
318
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
319
+ }
320
+ },
321
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
322
+ "version": "3.4.3",
323
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
324
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
325
+ "dev": true,
326
+ "license": "Apache-2.0",
327
+ "engines": {
328
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
329
+ },
330
+ "funding": {
331
+ "url": "https://opencollective.com/eslint"
332
+ }
333
+ },
334
+ "node_modules/@eslint-community/regexpp": {
335
+ "version": "4.12.2",
336
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
337
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
338
+ "dev": true,
339
+ "license": "MIT",
340
+ "engines": {
341
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
342
+ }
343
+ },
344
+ "node_modules/@eslint/config-array": {
345
+ "version": "0.21.2",
346
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
347
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
348
+ "dev": true,
349
+ "license": "Apache-2.0",
350
+ "dependencies": {
351
+ "@eslint/object-schema": "^2.1.7",
352
+ "debug": "^4.3.1",
353
+ "minimatch": "^3.1.5"
354
+ },
355
+ "engines": {
356
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
357
+ }
358
+ },
359
+ "node_modules/@eslint/config-helpers": {
360
+ "version": "0.4.2",
361
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
362
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
363
+ "dev": true,
364
+ "license": "Apache-2.0",
365
+ "dependencies": {
366
+ "@eslint/core": "^0.17.0"
367
+ },
368
+ "engines": {
369
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
370
+ }
371
+ },
372
+ "node_modules/@eslint/core": {
373
+ "version": "0.17.0",
374
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
375
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
376
+ "dev": true,
377
+ "license": "Apache-2.0",
378
+ "dependencies": {
379
+ "@types/json-schema": "^7.0.15"
380
+ },
381
+ "engines": {
382
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
383
+ }
384
+ },
385
+ "node_modules/@eslint/eslintrc": {
386
+ "version": "3.3.5",
387
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
388
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
389
+ "dev": true,
390
+ "license": "MIT",
391
+ "dependencies": {
392
+ "ajv": "^6.14.0",
393
+ "debug": "^4.3.2",
394
+ "espree": "^10.0.1",
395
+ "globals": "^14.0.0",
396
+ "ignore": "^5.2.0",
397
+ "import-fresh": "^3.2.1",
398
+ "js-yaml": "^4.1.1",
399
+ "minimatch": "^3.1.5",
400
+ "strip-json-comments": "^3.1.1"
401
+ },
402
+ "engines": {
403
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
404
+ },
405
+ "funding": {
406
+ "url": "https://opencollective.com/eslint"
407
+ }
408
+ },
409
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
410
+ "version": "14.0.0",
411
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
412
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
413
+ "dev": true,
414
+ "license": "MIT",
415
+ "engines": {
416
+ "node": ">=18"
417
+ },
418
+ "funding": {
419
+ "url": "https://github.com/sponsors/sindresorhus"
420
+ }
421
+ },
422
+ "node_modules/@eslint/js": {
423
+ "version": "9.39.4",
424
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
425
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
426
+ "dev": true,
427
+ "license": "MIT",
428
+ "engines": {
429
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
430
+ },
431
+ "funding": {
432
+ "url": "https://eslint.org/donate"
433
+ }
434
+ },
435
+ "node_modules/@eslint/object-schema": {
436
+ "version": "2.1.7",
437
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
438
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
439
+ "dev": true,
440
+ "license": "Apache-2.0",
441
+ "engines": {
442
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
443
+ }
444
+ },
445
+ "node_modules/@eslint/plugin-kit": {
446
+ "version": "0.4.1",
447
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
448
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
449
+ "dev": true,
450
+ "license": "Apache-2.0",
451
+ "dependencies": {
452
+ "@eslint/core": "^0.17.0",
453
+ "levn": "^0.4.1"
454
+ },
455
+ "engines": {
456
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
457
+ }
458
+ },
459
+ "node_modules/@humanfs/core": {
460
+ "version": "0.19.1",
461
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
462
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
463
+ "dev": true,
464
+ "license": "Apache-2.0",
465
+ "engines": {
466
+ "node": ">=18.18.0"
467
+ }
468
+ },
469
+ "node_modules/@humanfs/node": {
470
+ "version": "0.16.7",
471
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
472
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
473
+ "dev": true,
474
+ "license": "Apache-2.0",
475
+ "dependencies": {
476
+ "@humanfs/core": "^0.19.1",
477
+ "@humanwhocodes/retry": "^0.4.0"
478
+ },
479
+ "engines": {
480
+ "node": ">=18.18.0"
481
+ }
482
+ },
483
+ "node_modules/@humanwhocodes/module-importer": {
484
+ "version": "1.0.1",
485
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
486
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
487
+ "dev": true,
488
+ "license": "Apache-2.0",
489
+ "engines": {
490
+ "node": ">=12.22"
491
+ },
492
+ "funding": {
493
+ "type": "github",
494
+ "url": "https://github.com/sponsors/nzakas"
495
+ }
496
+ },
497
+ "node_modules/@humanwhocodes/retry": {
498
+ "version": "0.4.3",
499
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
500
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
501
+ "dev": true,
502
+ "license": "Apache-2.0",
503
+ "engines": {
504
+ "node": ">=18.18"
505
+ },
506
+ "funding": {
507
+ "type": "github",
508
+ "url": "https://github.com/sponsors/nzakas"
509
+ }
510
+ },
511
+ "node_modules/@jridgewell/gen-mapping": {
512
+ "version": "0.3.13",
513
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
514
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
515
+ "dev": true,
516
+ "license": "MIT",
517
+ "dependencies": {
518
+ "@jridgewell/sourcemap-codec": "^1.5.0",
519
+ "@jridgewell/trace-mapping": "^0.3.24"
520
+ }
521
+ },
522
+ "node_modules/@jridgewell/remapping": {
523
+ "version": "2.3.5",
524
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
525
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
526
+ "dev": true,
527
+ "license": "MIT",
528
+ "dependencies": {
529
+ "@jridgewell/gen-mapping": "^0.3.5",
530
+ "@jridgewell/trace-mapping": "^0.3.24"
531
+ }
532
+ },
533
+ "node_modules/@jridgewell/resolve-uri": {
534
+ "version": "3.1.2",
535
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
536
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
537
+ "dev": true,
538
+ "license": "MIT",
539
+ "engines": {
540
+ "node": ">=6.0.0"
541
+ }
542
+ },
543
+ "node_modules/@jridgewell/sourcemap-codec": {
544
+ "version": "1.5.5",
545
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
546
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
547
+ "dev": true,
548
+ "license": "MIT"
549
+ },
550
+ "node_modules/@jridgewell/trace-mapping": {
551
+ "version": "0.3.31",
552
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
553
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
554
+ "dev": true,
555
+ "license": "MIT",
556
+ "dependencies": {
557
+ "@jridgewell/resolve-uri": "^3.1.0",
558
+ "@jridgewell/sourcemap-codec": "^1.4.14"
559
+ }
560
+ },
561
+ "node_modules/@napi-rs/wasm-runtime": {
562
+ "version": "1.1.3",
563
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
564
+ "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
565
+ "dev": true,
566
+ "license": "MIT",
567
+ "optional": true,
568
+ "dependencies": {
569
+ "@tybys/wasm-util": "^0.10.1"
570
+ },
571
+ "funding": {
572
+ "type": "github",
573
+ "url": "https://github.com/sponsors/Brooooooklyn"
574
+ },
575
+ "peerDependencies": {
576
+ "@emnapi/core": "^1.7.1",
577
+ "@emnapi/runtime": "^1.7.1"
578
+ }
579
+ },
580
+ "node_modules/@oxc-project/types": {
581
+ "version": "0.124.0",
582
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
583
+ "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
584
+ "dev": true,
585
+ "license": "MIT",
586
+ "funding": {
587
+ "url": "https://github.com/sponsors/Boshen"
588
+ }
589
+ },
590
+ "node_modules/@rolldown/binding-android-arm64": {
591
+ "version": "1.0.0-rc.15",
592
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
593
+ "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
594
+ "cpu": [
595
+ "arm64"
596
+ ],
597
+ "dev": true,
598
+ "license": "MIT",
599
+ "optional": true,
600
+ "os": [
601
+ "android"
602
+ ],
603
+ "engines": {
604
+ "node": "^20.19.0 || >=22.12.0"
605
+ }
606
+ },
607
+ "node_modules/@rolldown/binding-darwin-arm64": {
608
+ "version": "1.0.0-rc.15",
609
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
610
+ "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
611
+ "cpu": [
612
+ "arm64"
613
+ ],
614
+ "dev": true,
615
+ "license": "MIT",
616
+ "optional": true,
617
+ "os": [
618
+ "darwin"
619
+ ],
620
+ "engines": {
621
+ "node": "^20.19.0 || >=22.12.0"
622
+ }
623
+ },
624
+ "node_modules/@rolldown/binding-darwin-x64": {
625
+ "version": "1.0.0-rc.15",
626
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
627
+ "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
628
+ "cpu": [
629
+ "x64"
630
+ ],
631
+ "dev": true,
632
+ "license": "MIT",
633
+ "optional": true,
634
+ "os": [
635
+ "darwin"
636
+ ],
637
+ "engines": {
638
+ "node": "^20.19.0 || >=22.12.0"
639
+ }
640
+ },
641
+ "node_modules/@rolldown/binding-freebsd-x64": {
642
+ "version": "1.0.0-rc.15",
643
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
644
+ "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
645
+ "cpu": [
646
+ "x64"
647
+ ],
648
+ "dev": true,
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "freebsd"
653
+ ],
654
+ "engines": {
655
+ "node": "^20.19.0 || >=22.12.0"
656
+ }
657
+ },
658
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
659
+ "version": "1.0.0-rc.15",
660
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
661
+ "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
662
+ "cpu": [
663
+ "arm"
664
+ ],
665
+ "dev": true,
666
+ "license": "MIT",
667
+ "optional": true,
668
+ "os": [
669
+ "linux"
670
+ ],
671
+ "engines": {
672
+ "node": "^20.19.0 || >=22.12.0"
673
+ }
674
+ },
675
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
676
+ "version": "1.0.0-rc.15",
677
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
678
+ "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
679
+ "cpu": [
680
+ "arm64"
681
+ ],
682
+ "dev": true,
683
+ "license": "MIT",
684
+ "optional": true,
685
+ "os": [
686
+ "linux"
687
+ ],
688
+ "engines": {
689
+ "node": "^20.19.0 || >=22.12.0"
690
+ }
691
+ },
692
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
693
+ "version": "1.0.0-rc.15",
694
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
695
+ "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
696
+ "cpu": [
697
+ "arm64"
698
+ ],
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "optional": true,
702
+ "os": [
703
+ "linux"
704
+ ],
705
+ "engines": {
706
+ "node": "^20.19.0 || >=22.12.0"
707
+ }
708
+ },
709
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
710
+ "version": "1.0.0-rc.15",
711
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
712
+ "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
713
+ "cpu": [
714
+ "ppc64"
715
+ ],
716
+ "dev": true,
717
+ "license": "MIT",
718
+ "optional": true,
719
+ "os": [
720
+ "linux"
721
+ ],
722
+ "engines": {
723
+ "node": "^20.19.0 || >=22.12.0"
724
+ }
725
+ },
726
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
727
+ "version": "1.0.0-rc.15",
728
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
729
+ "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
730
+ "cpu": [
731
+ "s390x"
732
+ ],
733
+ "dev": true,
734
+ "license": "MIT",
735
+ "optional": true,
736
+ "os": [
737
+ "linux"
738
+ ],
739
+ "engines": {
740
+ "node": "^20.19.0 || >=22.12.0"
741
+ }
742
+ },
743
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
744
+ "version": "1.0.0-rc.15",
745
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
746
+ "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
747
+ "cpu": [
748
+ "x64"
749
+ ],
750
+ "dev": true,
751
+ "license": "MIT",
752
+ "optional": true,
753
+ "os": [
754
+ "linux"
755
+ ],
756
+ "engines": {
757
+ "node": "^20.19.0 || >=22.12.0"
758
+ }
759
+ },
760
+ "node_modules/@rolldown/binding-linux-x64-musl": {
761
+ "version": "1.0.0-rc.15",
762
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
763
+ "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
764
+ "cpu": [
765
+ "x64"
766
+ ],
767
+ "dev": true,
768
+ "license": "MIT",
769
+ "optional": true,
770
+ "os": [
771
+ "linux"
772
+ ],
773
+ "engines": {
774
+ "node": "^20.19.0 || >=22.12.0"
775
+ }
776
+ },
777
+ "node_modules/@rolldown/binding-openharmony-arm64": {
778
+ "version": "1.0.0-rc.15",
779
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
780
+ "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
781
+ "cpu": [
782
+ "arm64"
783
+ ],
784
+ "dev": true,
785
+ "license": "MIT",
786
+ "optional": true,
787
+ "os": [
788
+ "openharmony"
789
+ ],
790
+ "engines": {
791
+ "node": "^20.19.0 || >=22.12.0"
792
+ }
793
+ },
794
+ "node_modules/@rolldown/binding-wasm32-wasi": {
795
+ "version": "1.0.0-rc.15",
796
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
797
+ "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
798
+ "cpu": [
799
+ "wasm32"
800
+ ],
801
+ "dev": true,
802
+ "license": "MIT",
803
+ "optional": true,
804
+ "dependencies": {
805
+ "@emnapi/core": "1.9.2",
806
+ "@emnapi/runtime": "1.9.2",
807
+ "@napi-rs/wasm-runtime": "^1.1.3"
808
+ },
809
+ "engines": {
810
+ "node": ">=14.0.0"
811
+ }
812
+ },
813
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
814
+ "version": "1.0.0-rc.15",
815
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
816
+ "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
817
+ "cpu": [
818
+ "arm64"
819
+ ],
820
+ "dev": true,
821
+ "license": "MIT",
822
+ "optional": true,
823
+ "os": [
824
+ "win32"
825
+ ],
826
+ "engines": {
827
+ "node": "^20.19.0 || >=22.12.0"
828
+ }
829
+ },
830
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
831
+ "version": "1.0.0-rc.15",
832
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
833
+ "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
834
+ "cpu": [
835
+ "x64"
836
+ ],
837
+ "dev": true,
838
+ "license": "MIT",
839
+ "optional": true,
840
+ "os": [
841
+ "win32"
842
+ ],
843
+ "engines": {
844
+ "node": "^20.19.0 || >=22.12.0"
845
+ }
846
+ },
847
+ "node_modules/@rolldown/pluginutils": {
848
+ "version": "1.0.0-rc.7",
849
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
850
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
851
+ "dev": true,
852
+ "license": "MIT"
853
+ },
854
+ "node_modules/@tybys/wasm-util": {
855
+ "version": "0.10.1",
856
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
857
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
858
+ "dev": true,
859
+ "license": "MIT",
860
+ "optional": true,
861
+ "dependencies": {
862
+ "tslib": "^2.4.0"
863
+ }
864
+ },
865
+ "node_modules/@types/estree": {
866
+ "version": "1.0.8",
867
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
868
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
869
+ "dev": true,
870
+ "license": "MIT"
871
+ },
872
+ "node_modules/@types/json-schema": {
873
+ "version": "7.0.15",
874
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
875
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
876
+ "dev": true,
877
+ "license": "MIT"
878
+ },
879
+ "node_modules/@types/react": {
880
+ "version": "19.2.14",
881
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
882
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
883
+ "dev": true,
884
+ "license": "MIT",
885
+ "dependencies": {
886
+ "csstype": "^3.2.2"
887
+ }
888
+ },
889
+ "node_modules/@types/react-dom": {
890
+ "version": "19.2.3",
891
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
892
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
893
+ "dev": true,
894
+ "license": "MIT",
895
+ "peerDependencies": {
896
+ "@types/react": "^19.2.0"
897
+ }
898
+ },
899
+ "node_modules/@vitejs/plugin-react": {
900
+ "version": "6.0.1",
901
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
902
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
903
+ "dev": true,
904
+ "license": "MIT",
905
+ "dependencies": {
906
+ "@rolldown/pluginutils": "1.0.0-rc.7"
907
+ },
908
+ "engines": {
909
+ "node": "^20.19.0 || >=22.12.0"
910
+ },
911
+ "peerDependencies": {
912
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
913
+ "babel-plugin-react-compiler": "^1.0.0",
914
+ "vite": "^8.0.0"
915
+ },
916
+ "peerDependenciesMeta": {
917
+ "@rolldown/plugin-babel": {
918
+ "optional": true
919
+ },
920
+ "babel-plugin-react-compiler": {
921
+ "optional": true
922
+ }
923
+ }
924
+ },
925
+ "node_modules/acorn": {
926
+ "version": "8.16.0",
927
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
928
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
929
+ "dev": true,
930
+ "license": "MIT",
931
+ "bin": {
932
+ "acorn": "bin/acorn"
933
+ },
934
+ "engines": {
935
+ "node": ">=0.4.0"
936
+ }
937
+ },
938
+ "node_modules/acorn-jsx": {
939
+ "version": "5.3.2",
940
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
941
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
942
+ "dev": true,
943
+ "license": "MIT",
944
+ "peerDependencies": {
945
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
946
+ }
947
+ },
948
+ "node_modules/ajv": {
949
+ "version": "6.14.0",
950
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
951
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
952
+ "dev": true,
953
+ "license": "MIT",
954
+ "dependencies": {
955
+ "fast-deep-equal": "^3.1.1",
956
+ "fast-json-stable-stringify": "^2.0.0",
957
+ "json-schema-traverse": "^0.4.1",
958
+ "uri-js": "^4.2.2"
959
+ },
960
+ "funding": {
961
+ "type": "github",
962
+ "url": "https://github.com/sponsors/epoberezkin"
963
+ }
964
+ },
965
+ "node_modules/ansi-styles": {
966
+ "version": "4.3.0",
967
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
968
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
969
+ "dev": true,
970
+ "license": "MIT",
971
+ "dependencies": {
972
+ "color-convert": "^2.0.1"
973
+ },
974
+ "engines": {
975
+ "node": ">=8"
976
+ },
977
+ "funding": {
978
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
979
+ }
980
+ },
981
+ "node_modules/argparse": {
982
+ "version": "2.0.1",
983
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
984
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
985
+ "dev": true,
986
+ "license": "Python-2.0"
987
+ },
988
+ "node_modules/balanced-match": {
989
+ "version": "1.0.2",
990
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
991
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
992
+ "dev": true,
993
+ "license": "MIT"
994
+ },
995
+ "node_modules/baseline-browser-mapping": {
996
+ "version": "2.10.18",
997
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
998
+ "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==",
999
+ "dev": true,
1000
+ "license": "Apache-2.0",
1001
+ "bin": {
1002
+ "baseline-browser-mapping": "dist/cli.cjs"
1003
+ },
1004
+ "engines": {
1005
+ "node": ">=6.0.0"
1006
+ }
1007
+ },
1008
+ "node_modules/brace-expansion": {
1009
+ "version": "1.1.14",
1010
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
1011
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
1012
+ "dev": true,
1013
+ "license": "MIT",
1014
+ "dependencies": {
1015
+ "balanced-match": "^1.0.0",
1016
+ "concat-map": "0.0.1"
1017
+ }
1018
+ },
1019
+ "node_modules/browserslist": {
1020
+ "version": "4.28.2",
1021
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1022
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1023
+ "dev": true,
1024
+ "funding": [
1025
+ {
1026
+ "type": "opencollective",
1027
+ "url": "https://opencollective.com/browserslist"
1028
+ },
1029
+ {
1030
+ "type": "tidelift",
1031
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1032
+ },
1033
+ {
1034
+ "type": "github",
1035
+ "url": "https://github.com/sponsors/ai"
1036
+ }
1037
+ ],
1038
+ "license": "MIT",
1039
+ "dependencies": {
1040
+ "baseline-browser-mapping": "^2.10.12",
1041
+ "caniuse-lite": "^1.0.30001782",
1042
+ "electron-to-chromium": "^1.5.328",
1043
+ "node-releases": "^2.0.36",
1044
+ "update-browserslist-db": "^1.2.3"
1045
+ },
1046
+ "bin": {
1047
+ "browserslist": "cli.js"
1048
+ },
1049
+ "engines": {
1050
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1051
+ }
1052
+ },
1053
+ "node_modules/callsites": {
1054
+ "version": "3.1.0",
1055
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1056
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1057
+ "dev": true,
1058
+ "license": "MIT",
1059
+ "engines": {
1060
+ "node": ">=6"
1061
+ }
1062
+ },
1063
+ "node_modules/caniuse-lite": {
1064
+ "version": "1.0.30001787",
1065
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
1066
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
1067
+ "dev": true,
1068
+ "funding": [
1069
+ {
1070
+ "type": "opencollective",
1071
+ "url": "https://opencollective.com/browserslist"
1072
+ },
1073
+ {
1074
+ "type": "tidelift",
1075
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1076
+ },
1077
+ {
1078
+ "type": "github",
1079
+ "url": "https://github.com/sponsors/ai"
1080
+ }
1081
+ ],
1082
+ "license": "CC-BY-4.0"
1083
+ },
1084
+ "node_modules/chalk": {
1085
+ "version": "4.1.2",
1086
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
1087
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
1088
+ "dev": true,
1089
+ "license": "MIT",
1090
+ "dependencies": {
1091
+ "ansi-styles": "^4.1.0",
1092
+ "supports-color": "^7.1.0"
1093
+ },
1094
+ "engines": {
1095
+ "node": ">=10"
1096
+ },
1097
+ "funding": {
1098
+ "url": "https://github.com/chalk/chalk?sponsor=1"
1099
+ }
1100
+ },
1101
+ "node_modules/color-convert": {
1102
+ "version": "2.0.1",
1103
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1104
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1105
+ "dev": true,
1106
+ "license": "MIT",
1107
+ "dependencies": {
1108
+ "color-name": "~1.1.4"
1109
+ },
1110
+ "engines": {
1111
+ "node": ">=7.0.0"
1112
+ }
1113
+ },
1114
+ "node_modules/color-name": {
1115
+ "version": "1.1.4",
1116
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1117
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1118
+ "dev": true,
1119
+ "license": "MIT"
1120
+ },
1121
+ "node_modules/concat-map": {
1122
+ "version": "0.0.1",
1123
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
1124
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
1125
+ "dev": true,
1126
+ "license": "MIT"
1127
+ },
1128
+ "node_modules/convert-source-map": {
1129
+ "version": "2.0.0",
1130
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1131
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1132
+ "dev": true,
1133
+ "license": "MIT"
1134
+ },
1135
+ "node_modules/cross-spawn": {
1136
+ "version": "7.0.6",
1137
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1138
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1139
+ "dev": true,
1140
+ "license": "MIT",
1141
+ "dependencies": {
1142
+ "path-key": "^3.1.0",
1143
+ "shebang-command": "^2.0.0",
1144
+ "which": "^2.0.1"
1145
+ },
1146
+ "engines": {
1147
+ "node": ">= 8"
1148
+ }
1149
+ },
1150
+ "node_modules/csstype": {
1151
+ "version": "3.2.3",
1152
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1153
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1154
+ "license": "MIT"
1155
+ },
1156
+ "node_modules/debug": {
1157
+ "version": "4.4.3",
1158
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1159
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1160
+ "dev": true,
1161
+ "license": "MIT",
1162
+ "dependencies": {
1163
+ "ms": "^2.1.3"
1164
+ },
1165
+ "engines": {
1166
+ "node": ">=6.0"
1167
+ },
1168
+ "peerDependenciesMeta": {
1169
+ "supports-color": {
1170
+ "optional": true
1171
+ }
1172
+ }
1173
+ },
1174
+ "node_modules/deep-is": {
1175
+ "version": "0.1.4",
1176
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
1177
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1178
+ "dev": true,
1179
+ "license": "MIT"
1180
+ },
1181
+ "node_modules/detect-libc": {
1182
+ "version": "2.1.2",
1183
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1184
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1185
+ "dev": true,
1186
+ "license": "Apache-2.0",
1187
+ "engines": {
1188
+ "node": ">=8"
1189
+ }
1190
+ },
1191
+ "node_modules/electron-to-chromium": {
1192
+ "version": "1.5.335",
1193
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
1194
+ "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==",
1195
+ "dev": true,
1196
+ "license": "ISC"
1197
+ },
1198
+ "node_modules/escalade": {
1199
+ "version": "3.2.0",
1200
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1201
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1202
+ "dev": true,
1203
+ "license": "MIT",
1204
+ "engines": {
1205
+ "node": ">=6"
1206
+ }
1207
+ },
1208
+ "node_modules/escape-string-regexp": {
1209
+ "version": "4.0.0",
1210
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1211
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1212
+ "dev": true,
1213
+ "license": "MIT",
1214
+ "engines": {
1215
+ "node": ">=10"
1216
+ },
1217
+ "funding": {
1218
+ "url": "https://github.com/sponsors/sindresorhus"
1219
+ }
1220
+ },
1221
+ "node_modules/eslint": {
1222
+ "version": "9.39.4",
1223
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
1224
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
1225
+ "dev": true,
1226
+ "license": "MIT",
1227
+ "dependencies": {
1228
+ "@eslint-community/eslint-utils": "^4.8.0",
1229
+ "@eslint-community/regexpp": "^4.12.1",
1230
+ "@eslint/config-array": "^0.21.2",
1231
+ "@eslint/config-helpers": "^0.4.2",
1232
+ "@eslint/core": "^0.17.0",
1233
+ "@eslint/eslintrc": "^3.3.5",
1234
+ "@eslint/js": "9.39.4",
1235
+ "@eslint/plugin-kit": "^0.4.1",
1236
+ "@humanfs/node": "^0.16.6",
1237
+ "@humanwhocodes/module-importer": "^1.0.1",
1238
+ "@humanwhocodes/retry": "^0.4.2",
1239
+ "@types/estree": "^1.0.6",
1240
+ "ajv": "^6.14.0",
1241
+ "chalk": "^4.0.0",
1242
+ "cross-spawn": "^7.0.6",
1243
+ "debug": "^4.3.2",
1244
+ "escape-string-regexp": "^4.0.0",
1245
+ "eslint-scope": "^8.4.0",
1246
+ "eslint-visitor-keys": "^4.2.1",
1247
+ "espree": "^10.4.0",
1248
+ "esquery": "^1.5.0",
1249
+ "esutils": "^2.0.2",
1250
+ "fast-deep-equal": "^3.1.3",
1251
+ "file-entry-cache": "^8.0.0",
1252
+ "find-up": "^5.0.0",
1253
+ "glob-parent": "^6.0.2",
1254
+ "ignore": "^5.2.0",
1255
+ "imurmurhash": "^0.1.4",
1256
+ "is-glob": "^4.0.0",
1257
+ "json-stable-stringify-without-jsonify": "^1.0.1",
1258
+ "lodash.merge": "^4.6.2",
1259
+ "minimatch": "^3.1.5",
1260
+ "natural-compare": "^1.4.0",
1261
+ "optionator": "^0.9.3"
1262
+ },
1263
+ "bin": {
1264
+ "eslint": "bin/eslint.js"
1265
+ },
1266
+ "engines": {
1267
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1268
+ },
1269
+ "funding": {
1270
+ "url": "https://eslint.org/donate"
1271
+ },
1272
+ "peerDependencies": {
1273
+ "jiti": "*"
1274
+ },
1275
+ "peerDependenciesMeta": {
1276
+ "jiti": {
1277
+ "optional": true
1278
+ }
1279
+ }
1280
+ },
1281
+ "node_modules/eslint-plugin-react-hooks": {
1282
+ "version": "7.0.1",
1283
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
1284
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
1285
+ "dev": true,
1286
+ "license": "MIT",
1287
+ "dependencies": {
1288
+ "@babel/core": "^7.24.4",
1289
+ "@babel/parser": "^7.24.4",
1290
+ "hermes-parser": "^0.25.1",
1291
+ "zod": "^3.25.0 || ^4.0.0",
1292
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
1293
+ },
1294
+ "engines": {
1295
+ "node": ">=18"
1296
+ },
1297
+ "peerDependencies": {
1298
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
1299
+ }
1300
+ },
1301
+ "node_modules/eslint-plugin-react-refresh": {
1302
+ "version": "0.5.2",
1303
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
1304
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
1305
+ "dev": true,
1306
+ "license": "MIT",
1307
+ "peerDependencies": {
1308
+ "eslint": "^9 || ^10"
1309
+ }
1310
+ },
1311
+ "node_modules/eslint-scope": {
1312
+ "version": "8.4.0",
1313
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
1314
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
1315
+ "dev": true,
1316
+ "license": "BSD-2-Clause",
1317
+ "dependencies": {
1318
+ "esrecurse": "^4.3.0",
1319
+ "estraverse": "^5.2.0"
1320
+ },
1321
+ "engines": {
1322
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1323
+ },
1324
+ "funding": {
1325
+ "url": "https://opencollective.com/eslint"
1326
+ }
1327
+ },
1328
+ "node_modules/eslint-visitor-keys": {
1329
+ "version": "4.2.1",
1330
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
1331
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
1332
+ "dev": true,
1333
+ "license": "Apache-2.0",
1334
+ "engines": {
1335
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1336
+ },
1337
+ "funding": {
1338
+ "url": "https://opencollective.com/eslint"
1339
+ }
1340
+ },
1341
+ "node_modules/espree": {
1342
+ "version": "10.4.0",
1343
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
1344
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
1345
+ "dev": true,
1346
+ "license": "BSD-2-Clause",
1347
+ "dependencies": {
1348
+ "acorn": "^8.15.0",
1349
+ "acorn-jsx": "^5.3.2",
1350
+ "eslint-visitor-keys": "^4.2.1"
1351
+ },
1352
+ "engines": {
1353
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1354
+ },
1355
+ "funding": {
1356
+ "url": "https://opencollective.com/eslint"
1357
+ }
1358
+ },
1359
+ "node_modules/esquery": {
1360
+ "version": "1.7.0",
1361
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
1362
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
1363
+ "dev": true,
1364
+ "license": "BSD-3-Clause",
1365
+ "dependencies": {
1366
+ "estraverse": "^5.1.0"
1367
+ },
1368
+ "engines": {
1369
+ "node": ">=0.10"
1370
+ }
1371
+ },
1372
+ "node_modules/esrecurse": {
1373
+ "version": "4.3.0",
1374
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
1375
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
1376
+ "dev": true,
1377
+ "license": "BSD-2-Clause",
1378
+ "dependencies": {
1379
+ "estraverse": "^5.2.0"
1380
+ },
1381
+ "engines": {
1382
+ "node": ">=4.0"
1383
+ }
1384
+ },
1385
+ "node_modules/estraverse": {
1386
+ "version": "5.3.0",
1387
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
1388
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
1389
+ "dev": true,
1390
+ "license": "BSD-2-Clause",
1391
+ "engines": {
1392
+ "node": ">=4.0"
1393
+ }
1394
+ },
1395
+ "node_modules/esutils": {
1396
+ "version": "2.0.3",
1397
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1398
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1399
+ "dev": true,
1400
+ "license": "BSD-2-Clause",
1401
+ "engines": {
1402
+ "node": ">=0.10.0"
1403
+ }
1404
+ },
1405
+ "node_modules/fast-deep-equal": {
1406
+ "version": "3.1.3",
1407
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1408
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1409
+ "dev": true,
1410
+ "license": "MIT"
1411
+ },
1412
+ "node_modules/fast-json-stable-stringify": {
1413
+ "version": "2.1.0",
1414
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
1415
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
1416
+ "dev": true,
1417
+ "license": "MIT"
1418
+ },
1419
+ "node_modules/fast-levenshtein": {
1420
+ "version": "2.0.6",
1421
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
1422
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
1423
+ "dev": true,
1424
+ "license": "MIT"
1425
+ },
1426
+ "node_modules/fdir": {
1427
+ "version": "6.5.0",
1428
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1429
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1430
+ "dev": true,
1431
+ "license": "MIT",
1432
+ "engines": {
1433
+ "node": ">=12.0.0"
1434
+ },
1435
+ "peerDependencies": {
1436
+ "picomatch": "^3 || ^4"
1437
+ },
1438
+ "peerDependenciesMeta": {
1439
+ "picomatch": {
1440
+ "optional": true
1441
+ }
1442
+ }
1443
+ },
1444
+ "node_modules/file-entry-cache": {
1445
+ "version": "8.0.0",
1446
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
1447
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
1448
+ "dev": true,
1449
+ "license": "MIT",
1450
+ "dependencies": {
1451
+ "flat-cache": "^4.0.0"
1452
+ },
1453
+ "engines": {
1454
+ "node": ">=16.0.0"
1455
+ }
1456
+ },
1457
+ "node_modules/find-up": {
1458
+ "version": "5.0.0",
1459
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
1460
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
1461
+ "dev": true,
1462
+ "license": "MIT",
1463
+ "dependencies": {
1464
+ "locate-path": "^6.0.0",
1465
+ "path-exists": "^4.0.0"
1466
+ },
1467
+ "engines": {
1468
+ "node": ">=10"
1469
+ },
1470
+ "funding": {
1471
+ "url": "https://github.com/sponsors/sindresorhus"
1472
+ }
1473
+ },
1474
+ "node_modules/flat-cache": {
1475
+ "version": "4.0.1",
1476
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
1477
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
1478
+ "dev": true,
1479
+ "license": "MIT",
1480
+ "dependencies": {
1481
+ "flatted": "^3.2.9",
1482
+ "keyv": "^4.5.4"
1483
+ },
1484
+ "engines": {
1485
+ "node": ">=16"
1486
+ }
1487
+ },
1488
+ "node_modules/flatted": {
1489
+ "version": "3.4.2",
1490
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
1491
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
1492
+ "dev": true,
1493
+ "license": "ISC"
1494
+ },
1495
+ "node_modules/fsevents": {
1496
+ "version": "2.3.3",
1497
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1498
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1499
+ "dev": true,
1500
+ "hasInstallScript": true,
1501
+ "license": "MIT",
1502
+ "optional": true,
1503
+ "os": [
1504
+ "darwin"
1505
+ ],
1506
+ "engines": {
1507
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1508
+ }
1509
+ },
1510
+ "node_modules/gensync": {
1511
+ "version": "1.0.0-beta.2",
1512
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1513
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1514
+ "dev": true,
1515
+ "license": "MIT",
1516
+ "engines": {
1517
+ "node": ">=6.9.0"
1518
+ }
1519
+ },
1520
+ "node_modules/glob-parent": {
1521
+ "version": "6.0.2",
1522
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1523
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1524
+ "dev": true,
1525
+ "license": "ISC",
1526
+ "dependencies": {
1527
+ "is-glob": "^4.0.3"
1528
+ },
1529
+ "engines": {
1530
+ "node": ">=10.13.0"
1531
+ }
1532
+ },
1533
+ "node_modules/globals": {
1534
+ "version": "17.5.0",
1535
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
1536
+ "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
1537
+ "dev": true,
1538
+ "license": "MIT",
1539
+ "engines": {
1540
+ "node": ">=18"
1541
+ },
1542
+ "funding": {
1543
+ "url": "https://github.com/sponsors/sindresorhus"
1544
+ }
1545
+ },
1546
+ "node_modules/goober": {
1547
+ "version": "2.1.18",
1548
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
1549
+ "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
1550
+ "license": "MIT",
1551
+ "peerDependencies": {
1552
+ "csstype": "^3.0.10"
1553
+ }
1554
+ },
1555
+ "node_modules/has-flag": {
1556
+ "version": "4.0.0",
1557
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
1558
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
1559
+ "dev": true,
1560
+ "license": "MIT",
1561
+ "engines": {
1562
+ "node": ">=8"
1563
+ }
1564
+ },
1565
+ "node_modules/hermes-estree": {
1566
+ "version": "0.25.1",
1567
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
1568
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
1569
+ "dev": true,
1570
+ "license": "MIT"
1571
+ },
1572
+ "node_modules/hermes-parser": {
1573
+ "version": "0.25.1",
1574
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
1575
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
1576
+ "dev": true,
1577
+ "license": "MIT",
1578
+ "dependencies": {
1579
+ "hermes-estree": "0.25.1"
1580
+ }
1581
+ },
1582
+ "node_modules/ignore": {
1583
+ "version": "5.3.2",
1584
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
1585
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
1586
+ "dev": true,
1587
+ "license": "MIT",
1588
+ "engines": {
1589
+ "node": ">= 4"
1590
+ }
1591
+ },
1592
+ "node_modules/import-fresh": {
1593
+ "version": "3.3.1",
1594
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
1595
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
1596
+ "dev": true,
1597
+ "license": "MIT",
1598
+ "dependencies": {
1599
+ "parent-module": "^1.0.0",
1600
+ "resolve-from": "^4.0.0"
1601
+ },
1602
+ "engines": {
1603
+ "node": ">=6"
1604
+ },
1605
+ "funding": {
1606
+ "url": "https://github.com/sponsors/sindresorhus"
1607
+ }
1608
+ },
1609
+ "node_modules/imurmurhash": {
1610
+ "version": "0.1.4",
1611
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
1612
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
1613
+ "dev": true,
1614
+ "license": "MIT",
1615
+ "engines": {
1616
+ "node": ">=0.8.19"
1617
+ }
1618
+ },
1619
+ "node_modules/is-extglob": {
1620
+ "version": "2.1.1",
1621
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1622
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1623
+ "dev": true,
1624
+ "license": "MIT",
1625
+ "engines": {
1626
+ "node": ">=0.10.0"
1627
+ }
1628
+ },
1629
+ "node_modules/is-glob": {
1630
+ "version": "4.0.3",
1631
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1632
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1633
+ "dev": true,
1634
+ "license": "MIT",
1635
+ "dependencies": {
1636
+ "is-extglob": "^2.1.1"
1637
+ },
1638
+ "engines": {
1639
+ "node": ">=0.10.0"
1640
+ }
1641
+ },
1642
+ "node_modules/isexe": {
1643
+ "version": "2.0.0",
1644
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1645
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1646
+ "dev": true,
1647
+ "license": "ISC"
1648
+ },
1649
+ "node_modules/js-tokens": {
1650
+ "version": "4.0.0",
1651
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1652
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1653
+ "dev": true,
1654
+ "license": "MIT"
1655
+ },
1656
+ "node_modules/js-yaml": {
1657
+ "version": "4.1.1",
1658
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
1659
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
1660
+ "dev": true,
1661
+ "license": "MIT",
1662
+ "dependencies": {
1663
+ "argparse": "^2.0.1"
1664
+ },
1665
+ "bin": {
1666
+ "js-yaml": "bin/js-yaml.js"
1667
+ }
1668
+ },
1669
+ "node_modules/jsesc": {
1670
+ "version": "3.1.0",
1671
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1672
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1673
+ "dev": true,
1674
+ "license": "MIT",
1675
+ "bin": {
1676
+ "jsesc": "bin/jsesc"
1677
+ },
1678
+ "engines": {
1679
+ "node": ">=6"
1680
+ }
1681
+ },
1682
+ "node_modules/json-buffer": {
1683
+ "version": "3.0.1",
1684
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
1685
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
1686
+ "dev": true,
1687
+ "license": "MIT"
1688
+ },
1689
+ "node_modules/json-schema-traverse": {
1690
+ "version": "0.4.1",
1691
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
1692
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
1693
+ "dev": true,
1694
+ "license": "MIT"
1695
+ },
1696
+ "node_modules/json-stable-stringify-without-jsonify": {
1697
+ "version": "1.0.1",
1698
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
1699
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
1700
+ "dev": true,
1701
+ "license": "MIT"
1702
+ },
1703
+ "node_modules/json5": {
1704
+ "version": "2.2.3",
1705
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1706
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1707
+ "dev": true,
1708
+ "license": "MIT",
1709
+ "bin": {
1710
+ "json5": "lib/cli.js"
1711
+ },
1712
+ "engines": {
1713
+ "node": ">=6"
1714
+ }
1715
+ },
1716
+ "node_modules/keyv": {
1717
+ "version": "4.5.4",
1718
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
1719
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
1720
+ "dev": true,
1721
+ "license": "MIT",
1722
+ "dependencies": {
1723
+ "json-buffer": "3.0.1"
1724
+ }
1725
+ },
1726
+ "node_modules/levn": {
1727
+ "version": "0.4.1",
1728
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
1729
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
1730
+ "dev": true,
1731
+ "license": "MIT",
1732
+ "dependencies": {
1733
+ "prelude-ls": "^1.2.1",
1734
+ "type-check": "~0.4.0"
1735
+ },
1736
+ "engines": {
1737
+ "node": ">= 0.8.0"
1738
+ }
1739
+ },
1740
+ "node_modules/lightningcss": {
1741
+ "version": "1.32.0",
1742
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
1743
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
1744
+ "dev": true,
1745
+ "license": "MPL-2.0",
1746
+ "dependencies": {
1747
+ "detect-libc": "^2.0.3"
1748
+ },
1749
+ "engines": {
1750
+ "node": ">= 12.0.0"
1751
+ },
1752
+ "funding": {
1753
+ "type": "opencollective",
1754
+ "url": "https://opencollective.com/parcel"
1755
+ },
1756
+ "optionalDependencies": {
1757
+ "lightningcss-android-arm64": "1.32.0",
1758
+ "lightningcss-darwin-arm64": "1.32.0",
1759
+ "lightningcss-darwin-x64": "1.32.0",
1760
+ "lightningcss-freebsd-x64": "1.32.0",
1761
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
1762
+ "lightningcss-linux-arm64-gnu": "1.32.0",
1763
+ "lightningcss-linux-arm64-musl": "1.32.0",
1764
+ "lightningcss-linux-x64-gnu": "1.32.0",
1765
+ "lightningcss-linux-x64-musl": "1.32.0",
1766
+ "lightningcss-win32-arm64-msvc": "1.32.0",
1767
+ "lightningcss-win32-x64-msvc": "1.32.0"
1768
+ }
1769
+ },
1770
+ "node_modules/lightningcss-android-arm64": {
1771
+ "version": "1.32.0",
1772
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
1773
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
1774
+ "cpu": [
1775
+ "arm64"
1776
+ ],
1777
+ "dev": true,
1778
+ "license": "MPL-2.0",
1779
+ "optional": true,
1780
+ "os": [
1781
+ "android"
1782
+ ],
1783
+ "engines": {
1784
+ "node": ">= 12.0.0"
1785
+ },
1786
+ "funding": {
1787
+ "type": "opencollective",
1788
+ "url": "https://opencollective.com/parcel"
1789
+ }
1790
+ },
1791
+ "node_modules/lightningcss-darwin-arm64": {
1792
+ "version": "1.32.0",
1793
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1794
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1795
+ "cpu": [
1796
+ "arm64"
1797
+ ],
1798
+ "dev": true,
1799
+ "license": "MPL-2.0",
1800
+ "optional": true,
1801
+ "os": [
1802
+ "darwin"
1803
+ ],
1804
+ "engines": {
1805
+ "node": ">= 12.0.0"
1806
+ },
1807
+ "funding": {
1808
+ "type": "opencollective",
1809
+ "url": "https://opencollective.com/parcel"
1810
+ }
1811
+ },
1812
+ "node_modules/lightningcss-darwin-x64": {
1813
+ "version": "1.32.0",
1814
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1815
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1816
+ "cpu": [
1817
+ "x64"
1818
+ ],
1819
+ "dev": true,
1820
+ "license": "MPL-2.0",
1821
+ "optional": true,
1822
+ "os": [
1823
+ "darwin"
1824
+ ],
1825
+ "engines": {
1826
+ "node": ">= 12.0.0"
1827
+ },
1828
+ "funding": {
1829
+ "type": "opencollective",
1830
+ "url": "https://opencollective.com/parcel"
1831
+ }
1832
+ },
1833
+ "node_modules/lightningcss-freebsd-x64": {
1834
+ "version": "1.32.0",
1835
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1836
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1837
+ "cpu": [
1838
+ "x64"
1839
+ ],
1840
+ "dev": true,
1841
+ "license": "MPL-2.0",
1842
+ "optional": true,
1843
+ "os": [
1844
+ "freebsd"
1845
+ ],
1846
+ "engines": {
1847
+ "node": ">= 12.0.0"
1848
+ },
1849
+ "funding": {
1850
+ "type": "opencollective",
1851
+ "url": "https://opencollective.com/parcel"
1852
+ }
1853
+ },
1854
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1855
+ "version": "1.32.0",
1856
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
1857
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
1858
+ "cpu": [
1859
+ "arm"
1860
+ ],
1861
+ "dev": true,
1862
+ "license": "MPL-2.0",
1863
+ "optional": true,
1864
+ "os": [
1865
+ "linux"
1866
+ ],
1867
+ "engines": {
1868
+ "node": ">= 12.0.0"
1869
+ },
1870
+ "funding": {
1871
+ "type": "opencollective",
1872
+ "url": "https://opencollective.com/parcel"
1873
+ }
1874
+ },
1875
+ "node_modules/lightningcss-linux-arm64-gnu": {
1876
+ "version": "1.32.0",
1877
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
1878
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
1879
+ "cpu": [
1880
+ "arm64"
1881
+ ],
1882
+ "dev": true,
1883
+ "license": "MPL-2.0",
1884
+ "optional": true,
1885
+ "os": [
1886
+ "linux"
1887
+ ],
1888
+ "engines": {
1889
+ "node": ">= 12.0.0"
1890
+ },
1891
+ "funding": {
1892
+ "type": "opencollective",
1893
+ "url": "https://opencollective.com/parcel"
1894
+ }
1895
+ },
1896
+ "node_modules/lightningcss-linux-arm64-musl": {
1897
+ "version": "1.32.0",
1898
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
1899
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
1900
+ "cpu": [
1901
+ "arm64"
1902
+ ],
1903
+ "dev": true,
1904
+ "license": "MPL-2.0",
1905
+ "optional": true,
1906
+ "os": [
1907
+ "linux"
1908
+ ],
1909
+ "engines": {
1910
+ "node": ">= 12.0.0"
1911
+ },
1912
+ "funding": {
1913
+ "type": "opencollective",
1914
+ "url": "https://opencollective.com/parcel"
1915
+ }
1916
+ },
1917
+ "node_modules/lightningcss-linux-x64-gnu": {
1918
+ "version": "1.32.0",
1919
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
1920
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
1921
+ "cpu": [
1922
+ "x64"
1923
+ ],
1924
+ "dev": true,
1925
+ "license": "MPL-2.0",
1926
+ "optional": true,
1927
+ "os": [
1928
+ "linux"
1929
+ ],
1930
+ "engines": {
1931
+ "node": ">= 12.0.0"
1932
+ },
1933
+ "funding": {
1934
+ "type": "opencollective",
1935
+ "url": "https://opencollective.com/parcel"
1936
+ }
1937
+ },
1938
+ "node_modules/lightningcss-linux-x64-musl": {
1939
+ "version": "1.32.0",
1940
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
1941
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
1942
+ "cpu": [
1943
+ "x64"
1944
+ ],
1945
+ "dev": true,
1946
+ "license": "MPL-2.0",
1947
+ "optional": true,
1948
+ "os": [
1949
+ "linux"
1950
+ ],
1951
+ "engines": {
1952
+ "node": ">= 12.0.0"
1953
+ },
1954
+ "funding": {
1955
+ "type": "opencollective",
1956
+ "url": "https://opencollective.com/parcel"
1957
+ }
1958
+ },
1959
+ "node_modules/lightningcss-win32-arm64-msvc": {
1960
+ "version": "1.32.0",
1961
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
1962
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
1963
+ "cpu": [
1964
+ "arm64"
1965
+ ],
1966
+ "dev": true,
1967
+ "license": "MPL-2.0",
1968
+ "optional": true,
1969
+ "os": [
1970
+ "win32"
1971
+ ],
1972
+ "engines": {
1973
+ "node": ">= 12.0.0"
1974
+ },
1975
+ "funding": {
1976
+ "type": "opencollective",
1977
+ "url": "https://opencollective.com/parcel"
1978
+ }
1979
+ },
1980
+ "node_modules/lightningcss-win32-x64-msvc": {
1981
+ "version": "1.32.0",
1982
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
1983
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
1984
+ "cpu": [
1985
+ "x64"
1986
+ ],
1987
+ "dev": true,
1988
+ "license": "MPL-2.0",
1989
+ "optional": true,
1990
+ "os": [
1991
+ "win32"
1992
+ ],
1993
+ "engines": {
1994
+ "node": ">= 12.0.0"
1995
+ },
1996
+ "funding": {
1997
+ "type": "opencollective",
1998
+ "url": "https://opencollective.com/parcel"
1999
+ }
2000
+ },
2001
+ "node_modules/locate-path": {
2002
+ "version": "6.0.0",
2003
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
2004
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
2005
+ "dev": true,
2006
+ "license": "MIT",
2007
+ "dependencies": {
2008
+ "p-locate": "^5.0.0"
2009
+ },
2010
+ "engines": {
2011
+ "node": ">=10"
2012
+ },
2013
+ "funding": {
2014
+ "url": "https://github.com/sponsors/sindresorhus"
2015
+ }
2016
+ },
2017
+ "node_modules/lodash.merge": {
2018
+ "version": "4.6.2",
2019
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
2020
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
2021
+ "dev": true,
2022
+ "license": "MIT"
2023
+ },
2024
+ "node_modules/lru-cache": {
2025
+ "version": "5.1.1",
2026
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2027
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2028
+ "dev": true,
2029
+ "license": "ISC",
2030
+ "dependencies": {
2031
+ "yallist": "^3.0.2"
2032
+ }
2033
+ },
2034
+ "node_modules/lucide-react": {
2035
+ "version": "1.8.0",
2036
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
2037
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
2038
+ "license": "ISC",
2039
+ "peerDependencies": {
2040
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2041
+ }
2042
+ },
2043
+ "node_modules/minimatch": {
2044
+ "version": "3.1.5",
2045
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
2046
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
2047
+ "dev": true,
2048
+ "license": "ISC",
2049
+ "dependencies": {
2050
+ "brace-expansion": "^1.1.7"
2051
+ },
2052
+ "engines": {
2053
+ "node": "*"
2054
+ }
2055
+ },
2056
+ "node_modules/ms": {
2057
+ "version": "2.1.3",
2058
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2059
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2060
+ "dev": true,
2061
+ "license": "MIT"
2062
+ },
2063
+ "node_modules/nanoid": {
2064
+ "version": "3.3.11",
2065
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
2066
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
2067
+ "dev": true,
2068
+ "funding": [
2069
+ {
2070
+ "type": "github",
2071
+ "url": "https://github.com/sponsors/ai"
2072
+ }
2073
+ ],
2074
+ "license": "MIT",
2075
+ "bin": {
2076
+ "nanoid": "bin/nanoid.cjs"
2077
+ },
2078
+ "engines": {
2079
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2080
+ }
2081
+ },
2082
+ "node_modules/natural-compare": {
2083
+ "version": "1.4.0",
2084
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
2085
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
2086
+ "dev": true,
2087
+ "license": "MIT"
2088
+ },
2089
+ "node_modules/node-releases": {
2090
+ "version": "2.0.37",
2091
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
2092
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
2093
+ "dev": true,
2094
+ "license": "MIT"
2095
+ },
2096
+ "node_modules/optionator": {
2097
+ "version": "0.9.4",
2098
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
2099
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
2100
+ "dev": true,
2101
+ "license": "MIT",
2102
+ "dependencies": {
2103
+ "deep-is": "^0.1.3",
2104
+ "fast-levenshtein": "^2.0.6",
2105
+ "levn": "^0.4.1",
2106
+ "prelude-ls": "^1.2.1",
2107
+ "type-check": "^0.4.0",
2108
+ "word-wrap": "^1.2.5"
2109
+ },
2110
+ "engines": {
2111
+ "node": ">= 0.8.0"
2112
+ }
2113
+ },
2114
+ "node_modules/p-limit": {
2115
+ "version": "3.1.0",
2116
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
2117
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
2118
+ "dev": true,
2119
+ "license": "MIT",
2120
+ "dependencies": {
2121
+ "yocto-queue": "^0.1.0"
2122
+ },
2123
+ "engines": {
2124
+ "node": ">=10"
2125
+ },
2126
+ "funding": {
2127
+ "url": "https://github.com/sponsors/sindresorhus"
2128
+ }
2129
+ },
2130
+ "node_modules/p-locate": {
2131
+ "version": "5.0.0",
2132
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
2133
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
2134
+ "dev": true,
2135
+ "license": "MIT",
2136
+ "dependencies": {
2137
+ "p-limit": "^3.0.2"
2138
+ },
2139
+ "engines": {
2140
+ "node": ">=10"
2141
+ },
2142
+ "funding": {
2143
+ "url": "https://github.com/sponsors/sindresorhus"
2144
+ }
2145
+ },
2146
+ "node_modules/parent-module": {
2147
+ "version": "1.0.1",
2148
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
2149
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
2150
+ "dev": true,
2151
+ "license": "MIT",
2152
+ "dependencies": {
2153
+ "callsites": "^3.0.0"
2154
+ },
2155
+ "engines": {
2156
+ "node": ">=6"
2157
+ }
2158
+ },
2159
+ "node_modules/path-exists": {
2160
+ "version": "4.0.0",
2161
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
2162
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
2163
+ "dev": true,
2164
+ "license": "MIT",
2165
+ "engines": {
2166
+ "node": ">=8"
2167
+ }
2168
+ },
2169
+ "node_modules/path-key": {
2170
+ "version": "3.1.1",
2171
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2172
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2173
+ "dev": true,
2174
+ "license": "MIT",
2175
+ "engines": {
2176
+ "node": ">=8"
2177
+ }
2178
+ },
2179
+ "node_modules/picocolors": {
2180
+ "version": "1.1.1",
2181
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2182
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2183
+ "dev": true,
2184
+ "license": "ISC"
2185
+ },
2186
+ "node_modules/picomatch": {
2187
+ "version": "4.0.4",
2188
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2189
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2190
+ "dev": true,
2191
+ "license": "MIT",
2192
+ "engines": {
2193
+ "node": ">=12"
2194
+ },
2195
+ "funding": {
2196
+ "url": "https://github.com/sponsors/jonschlinkert"
2197
+ }
2198
+ },
2199
+ "node_modules/postcss": {
2200
+ "version": "8.5.9",
2201
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
2202
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
2203
+ "dev": true,
2204
+ "funding": [
2205
+ {
2206
+ "type": "opencollective",
2207
+ "url": "https://opencollective.com/postcss/"
2208
+ },
2209
+ {
2210
+ "type": "tidelift",
2211
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2212
+ },
2213
+ {
2214
+ "type": "github",
2215
+ "url": "https://github.com/sponsors/ai"
2216
+ }
2217
+ ],
2218
+ "license": "MIT",
2219
+ "dependencies": {
2220
+ "nanoid": "^3.3.11",
2221
+ "picocolors": "^1.1.1",
2222
+ "source-map-js": "^1.2.1"
2223
+ },
2224
+ "engines": {
2225
+ "node": "^10 || ^12 || >=14"
2226
+ }
2227
+ },
2228
+ "node_modules/prelude-ls": {
2229
+ "version": "1.2.1",
2230
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
2231
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
2232
+ "dev": true,
2233
+ "license": "MIT",
2234
+ "engines": {
2235
+ "node": ">= 0.8.0"
2236
+ }
2237
+ },
2238
+ "node_modules/punycode": {
2239
+ "version": "2.3.1",
2240
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2241
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2242
+ "dev": true,
2243
+ "license": "MIT",
2244
+ "engines": {
2245
+ "node": ">=6"
2246
+ }
2247
+ },
2248
+ "node_modules/react": {
2249
+ "version": "19.2.5",
2250
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
2251
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
2252
+ "license": "MIT",
2253
+ "engines": {
2254
+ "node": ">=0.10.0"
2255
+ }
2256
+ },
2257
+ "node_modules/react-dom": {
2258
+ "version": "19.2.5",
2259
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
2260
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
2261
+ "license": "MIT",
2262
+ "dependencies": {
2263
+ "scheduler": "^0.27.0"
2264
+ },
2265
+ "peerDependencies": {
2266
+ "react": "^19.2.5"
2267
+ }
2268
+ },
2269
+ "node_modules/react-hot-toast": {
2270
+ "version": "2.6.0",
2271
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
2272
+ "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
2273
+ "license": "MIT",
2274
+ "dependencies": {
2275
+ "csstype": "^3.1.3",
2276
+ "goober": "^2.1.16"
2277
+ },
2278
+ "engines": {
2279
+ "node": ">=10"
2280
+ },
2281
+ "peerDependencies": {
2282
+ "react": ">=16",
2283
+ "react-dom": ">=16"
2284
+ }
2285
+ },
2286
+ "node_modules/resolve-from": {
2287
+ "version": "4.0.0",
2288
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
2289
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
2290
+ "dev": true,
2291
+ "license": "MIT",
2292
+ "engines": {
2293
+ "node": ">=4"
2294
+ }
2295
+ },
2296
+ "node_modules/rolldown": {
2297
+ "version": "1.0.0-rc.15",
2298
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
2299
+ "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
2300
+ "dev": true,
2301
+ "license": "MIT",
2302
+ "dependencies": {
2303
+ "@oxc-project/types": "=0.124.0",
2304
+ "@rolldown/pluginutils": "1.0.0-rc.15"
2305
+ },
2306
+ "bin": {
2307
+ "rolldown": "bin/cli.mjs"
2308
+ },
2309
+ "engines": {
2310
+ "node": "^20.19.0 || >=22.12.0"
2311
+ },
2312
+ "optionalDependencies": {
2313
+ "@rolldown/binding-android-arm64": "1.0.0-rc.15",
2314
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
2315
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.15",
2316
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
2317
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
2318
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
2319
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
2320
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
2321
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
2322
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
2323
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
2324
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
2325
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
2326
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
2327
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
2328
+ }
2329
+ },
2330
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
2331
+ "version": "1.0.0-rc.15",
2332
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
2333
+ "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
2334
+ "dev": true,
2335
+ "license": "MIT"
2336
+ },
2337
+ "node_modules/scheduler": {
2338
+ "version": "0.27.0",
2339
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2340
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2341
+ "license": "MIT"
2342
+ },
2343
+ "node_modules/semver": {
2344
+ "version": "6.3.1",
2345
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2346
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2347
+ "dev": true,
2348
+ "license": "ISC",
2349
+ "bin": {
2350
+ "semver": "bin/semver.js"
2351
+ }
2352
+ },
2353
+ "node_modules/shebang-command": {
2354
+ "version": "2.0.0",
2355
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2356
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2357
+ "dev": true,
2358
+ "license": "MIT",
2359
+ "dependencies": {
2360
+ "shebang-regex": "^3.0.0"
2361
+ },
2362
+ "engines": {
2363
+ "node": ">=8"
2364
+ }
2365
+ },
2366
+ "node_modules/shebang-regex": {
2367
+ "version": "3.0.0",
2368
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2369
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2370
+ "dev": true,
2371
+ "license": "MIT",
2372
+ "engines": {
2373
+ "node": ">=8"
2374
+ }
2375
+ },
2376
+ "node_modules/source-map-js": {
2377
+ "version": "1.2.1",
2378
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2379
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2380
+ "dev": true,
2381
+ "license": "BSD-3-Clause",
2382
+ "engines": {
2383
+ "node": ">=0.10.0"
2384
+ }
2385
+ },
2386
+ "node_modules/strip-json-comments": {
2387
+ "version": "3.1.1",
2388
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
2389
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
2390
+ "dev": true,
2391
+ "license": "MIT",
2392
+ "engines": {
2393
+ "node": ">=8"
2394
+ },
2395
+ "funding": {
2396
+ "url": "https://github.com/sponsors/sindresorhus"
2397
+ }
2398
+ },
2399
+ "node_modules/supports-color": {
2400
+ "version": "7.2.0",
2401
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
2402
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
2403
+ "dev": true,
2404
+ "license": "MIT",
2405
+ "dependencies": {
2406
+ "has-flag": "^4.0.0"
2407
+ },
2408
+ "engines": {
2409
+ "node": ">=8"
2410
+ }
2411
+ },
2412
+ "node_modules/tinyglobby": {
2413
+ "version": "0.2.16",
2414
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2415
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2416
+ "dev": true,
2417
+ "license": "MIT",
2418
+ "dependencies": {
2419
+ "fdir": "^6.5.0",
2420
+ "picomatch": "^4.0.4"
2421
+ },
2422
+ "engines": {
2423
+ "node": ">=12.0.0"
2424
+ },
2425
+ "funding": {
2426
+ "url": "https://github.com/sponsors/SuperchupuDev"
2427
+ }
2428
+ },
2429
+ "node_modules/tslib": {
2430
+ "version": "2.8.1",
2431
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2432
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2433
+ "dev": true,
2434
+ "license": "0BSD",
2435
+ "optional": true
2436
+ },
2437
+ "node_modules/type-check": {
2438
+ "version": "0.4.0",
2439
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
2440
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
2441
+ "dev": true,
2442
+ "license": "MIT",
2443
+ "dependencies": {
2444
+ "prelude-ls": "^1.2.1"
2445
+ },
2446
+ "engines": {
2447
+ "node": ">= 0.8.0"
2448
+ }
2449
+ },
2450
+ "node_modules/update-browserslist-db": {
2451
+ "version": "1.2.3",
2452
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2453
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2454
+ "dev": true,
2455
+ "funding": [
2456
+ {
2457
+ "type": "opencollective",
2458
+ "url": "https://opencollective.com/browserslist"
2459
+ },
2460
+ {
2461
+ "type": "tidelift",
2462
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2463
+ },
2464
+ {
2465
+ "type": "github",
2466
+ "url": "https://github.com/sponsors/ai"
2467
+ }
2468
+ ],
2469
+ "license": "MIT",
2470
+ "dependencies": {
2471
+ "escalade": "^3.2.0",
2472
+ "picocolors": "^1.1.1"
2473
+ },
2474
+ "bin": {
2475
+ "update-browserslist-db": "cli.js"
2476
+ },
2477
+ "peerDependencies": {
2478
+ "browserslist": ">= 4.21.0"
2479
+ }
2480
+ },
2481
+ "node_modules/uri-js": {
2482
+ "version": "4.4.1",
2483
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
2484
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
2485
+ "dev": true,
2486
+ "license": "BSD-2-Clause",
2487
+ "dependencies": {
2488
+ "punycode": "^2.1.0"
2489
+ }
2490
+ },
2491
+ "node_modules/vite": {
2492
+ "version": "8.0.8",
2493
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
2494
+ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
2495
+ "dev": true,
2496
+ "license": "MIT",
2497
+ "dependencies": {
2498
+ "lightningcss": "^1.32.0",
2499
+ "picomatch": "^4.0.4",
2500
+ "postcss": "^8.5.8",
2501
+ "rolldown": "1.0.0-rc.15",
2502
+ "tinyglobby": "^0.2.15"
2503
+ },
2504
+ "bin": {
2505
+ "vite": "bin/vite.js"
2506
+ },
2507
+ "engines": {
2508
+ "node": "^20.19.0 || >=22.12.0"
2509
+ },
2510
+ "funding": {
2511
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2512
+ },
2513
+ "optionalDependencies": {
2514
+ "fsevents": "~2.3.3"
2515
+ },
2516
+ "peerDependencies": {
2517
+ "@types/node": "^20.19.0 || >=22.12.0",
2518
+ "@vitejs/devtools": "^0.1.0",
2519
+ "esbuild": "^0.27.0 || ^0.28.0",
2520
+ "jiti": ">=1.21.0",
2521
+ "less": "^4.0.0",
2522
+ "sass": "^1.70.0",
2523
+ "sass-embedded": "^1.70.0",
2524
+ "stylus": ">=0.54.8",
2525
+ "sugarss": "^5.0.0",
2526
+ "terser": "^5.16.0",
2527
+ "tsx": "^4.8.1",
2528
+ "yaml": "^2.4.2"
2529
+ },
2530
+ "peerDependenciesMeta": {
2531
+ "@types/node": {
2532
+ "optional": true
2533
+ },
2534
+ "@vitejs/devtools": {
2535
+ "optional": true
2536
+ },
2537
+ "esbuild": {
2538
+ "optional": true
2539
+ },
2540
+ "jiti": {
2541
+ "optional": true
2542
+ },
2543
+ "less": {
2544
+ "optional": true
2545
+ },
2546
+ "sass": {
2547
+ "optional": true
2548
+ },
2549
+ "sass-embedded": {
2550
+ "optional": true
2551
+ },
2552
+ "stylus": {
2553
+ "optional": true
2554
+ },
2555
+ "sugarss": {
2556
+ "optional": true
2557
+ },
2558
+ "terser": {
2559
+ "optional": true
2560
+ },
2561
+ "tsx": {
2562
+ "optional": true
2563
+ },
2564
+ "yaml": {
2565
+ "optional": true
2566
+ }
2567
+ }
2568
+ },
2569
+ "node_modules/which": {
2570
+ "version": "2.0.2",
2571
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2572
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2573
+ "dev": true,
2574
+ "license": "ISC",
2575
+ "dependencies": {
2576
+ "isexe": "^2.0.0"
2577
+ },
2578
+ "bin": {
2579
+ "node-which": "bin/node-which"
2580
+ },
2581
+ "engines": {
2582
+ "node": ">= 8"
2583
+ }
2584
+ },
2585
+ "node_modules/word-wrap": {
2586
+ "version": "1.2.5",
2587
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
2588
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
2589
+ "dev": true,
2590
+ "license": "MIT",
2591
+ "engines": {
2592
+ "node": ">=0.10.0"
2593
+ }
2594
+ },
2595
+ "node_modules/yallist": {
2596
+ "version": "3.1.1",
2597
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2598
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2599
+ "dev": true,
2600
+ "license": "ISC"
2601
+ },
2602
+ "node_modules/yocto-queue": {
2603
+ "version": "0.1.0",
2604
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
2605
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
2606
+ "dev": true,
2607
+ "license": "MIT",
2608
+ "engines": {
2609
+ "node": ">=10"
2610
+ },
2611
+ "funding": {
2612
+ "url": "https://github.com/sponsors/sindresorhus"
2613
+ }
2614
+ },
2615
+ "node_modules/zod": {
2616
+ "version": "4.3.6",
2617
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
2618
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
2619
+ "dev": true,
2620
+ "license": "MIT",
2621
+ "funding": {
2622
+ "url": "https://github.com/sponsors/colinhacks"
2623
+ }
2624
+ },
2625
+ "node_modules/zod-validation-error": {
2626
+ "version": "4.0.2",
2627
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
2628
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
2629
+ "dev": true,
2630
+ "license": "MIT",
2631
+ "engines": {
2632
+ "node": ">=18.0.0"
2633
+ },
2634
+ "peerDependencies": {
2635
+ "zod": "^3.25.0 || ^4.0.0"
2636
+ }
2637
+ }
2638
+ }
2639
+ }
frontend/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lucide-react": "^1.8.0",
14
+ "react": "^19.2.4",
15
+ "react-dom": "^19.2.4",
16
+ "react-hot-toast": "^2.6.0"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.39.4",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.1",
23
+ "eslint": "^9.39.4",
24
+ "eslint-plugin-react-hooks": "^7.0.1",
25
+ "eslint-plugin-react-refresh": "^0.5.2",
26
+ "globals": "^17.4.0",
27
+ "vite": "^8.0.4"
28
+ }
29
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { Toaster } from 'react-hot-toast';
3
+ import { ThemeProvider } from './context/ThemeContext';
4
+ import { AuthProvider, useAuth } from './context/AuthContext';
5
+ import { FormProvider, useForm } from './context/FormContext';
6
+ import { fetchTemplateData, hydrateTemplateData } from './data/templateData';
7
+ import LoginPage from './components/auth/LoginPage';
8
+ import Navbar from './components/layout/Navbar';
9
+ import Sidebar from './components/layout/Sidebar';
10
+ import PatientHeader from './components/sections/PatientHeader';
11
+ import SectionRenderer from './components/sections/SectionRenderer';
12
+ import AbbreviationPanel from './components/reference/AbbreviationPanel';
13
+ import SmartPhrasesPanel from './components/reference/SmartPhrasesPanel';
14
+ import AIGeneratorPanel from './components/ai/AIGeneratorPanel';
15
+ import PreviewPanel from './components/preview/PreviewPanel';
16
+ import AddSectionModal from './components/modals/AddSectionModal';
17
+ import './index.css';
18
+
19
+ function AppContent({ templateSections }) {
20
+ const [activeSection, setActiveSection] = useState('_header');
21
+ const [showPreview, setShowPreview] = useState(false);
22
+ const [showGenerator, setShowGenerator] = useState(false);
23
+ const [showAddSection, setShowAddSection] = useState(false);
24
+ const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 1024);
25
+ const mainRef = useRef(null);
26
+ const { addCustomSection, formState } = useForm();
27
+
28
+ const isMobile = () => window.innerWidth <= 1024;
29
+
30
+ const handleSectionClick = (sectionId) => {
31
+ setActiveSection(sectionId);
32
+ const el = document.getElementById(`section-${sectionId}`);
33
+ if (el) {
34
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
35
+ }
36
+ // Auto-close sidebar on mobile after clicking a section
37
+ if (isMobile()) {
38
+ setSidebarOpen(false);
39
+ }
40
+ };
41
+
42
+ const handleAddSection = (title) => {
43
+ addCustomSection(title);
44
+ };
45
+
46
+ return (
47
+ <>
48
+ <Navbar
49
+ onPreview={() => setShowPreview(true)}
50
+ onGenerate={() => setShowGenerator(true)}
51
+ onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
52
+ />
53
+
54
+ <div className="app-layout">
55
+ {/* Backdrop overlay for mobile sidebar */}
56
+ {sidebarOpen && isMobile() && (
57
+ <div
58
+ className="sidebar-backdrop"
59
+ onClick={() => setSidebarOpen(false)}
60
+ />
61
+ )}
62
+
63
+ <Sidebar
64
+ activeSection={activeSection}
65
+ onSectionClick={handleSectionClick}
66
+ onAddSection={() => setShowAddSection(true)}
67
+ isOpen={sidebarOpen}
68
+ />
69
+
70
+ <main className="main-content" ref={mainRef}>
71
+ <div id="section-_header">
72
+ <PatientHeader />
73
+ </div>
74
+
75
+ <div id="section-_abbreviations">
76
+ <AbbreviationPanel />
77
+ </div>
78
+
79
+ <div id="section-_smart_phrases">
80
+ <SmartPhrasesPanel />
81
+ </div>
82
+
83
+ {templateSections.map(section => (
84
+ <SectionRenderer key={section.id} section={section} />
85
+ ))}
86
+
87
+ {formState.customSections.map(cs => (
88
+ <div key={cs.id} id={`section-${cs.id}`} className="section">
89
+ <div className="section__header">
90
+ <div className="section__header-icon" style={{ background: 'linear-gradient(135deg, var(--accent-500), var(--accent-700))' }}>
91
+ <span style={{ color: 'white', fontWeight: 700, fontSize: 'var(--font-sm)' }}>C</span>
92
+ </div>
93
+ <h2 className="section__title">{cs.title}</h2>
94
+ </div>
95
+ <div className="subsection">
96
+ <div className="subsection__title">
97
+ <span className="subsection__title-dot" />
98
+ Custom Content
99
+ </div>
100
+ <p style={{ color: 'var(--text-secondary)', fontSize: 'var(--font-sm)' }}>
101
+ This custom section will be included in AI generation. Add relevant notes below.
102
+ </p>
103
+ <div style={{ marginTop: 'var(--space-3)' }}>
104
+ <textarea
105
+ className="text-input__field"
106
+ placeholder="Enter custom notes, observations, or selections for this section..."
107
+ rows={4}
108
+ style={{ resize: 'vertical', width: '100%' }}
109
+ />
110
+ </div>
111
+ </div>
112
+ </div>
113
+ ))}
114
+
115
+ <div style={{ height: 80 }} />
116
+ </main>
117
+ </div>
118
+
119
+ <PreviewPanel isOpen={showPreview} onClose={() => setShowPreview(false)} />
120
+ <AIGeneratorPanel isOpen={showGenerator} onClose={() => setShowGenerator(false)} />
121
+ <AddSectionModal
122
+ isOpen={showAddSection}
123
+ onClose={() => setShowAddSection(false)}
124
+ onAdd={handleAddSection}
125
+ />
126
+
127
+ <Toaster
128
+ position="bottom-right"
129
+ toastOptions={{
130
+ style: {
131
+ background: 'var(--surface-card)',
132
+ color: 'var(--text-primary)',
133
+ border: '1px solid var(--border-primary)',
134
+ fontFamily: 'var(--font-family)',
135
+ fontSize: 'var(--font-sm)',
136
+ },
137
+ }}
138
+ />
139
+ </>
140
+ );
141
+ }
142
+
143
+ function AuthGate() {
144
+ const { isAuthenticated, loading, token } = useAuth();
145
+ const [templateLoaded, setTemplateLoaded] = useState(false);
146
+ const [templateSections, setTemplateSections] = useState([]);
147
+ const [templateError, setTemplateError] = useState('');
148
+
149
+ // Fetch template data once authenticated
150
+ useEffect(() => {
151
+ if (isAuthenticated && token && !templateLoaded) {
152
+ fetchTemplateData(token)
153
+ .then(data => {
154
+ const hydrated = hydrateTemplateData(data);
155
+ setTemplateSections(hydrated.TEMPLATE_SECTIONS);
156
+ setTemplateLoaded(true);
157
+ })
158
+ .catch(err => {
159
+ setTemplateError(err.message);
160
+ });
161
+ }
162
+ }, [isAuthenticated, token, templateLoaded]);
163
+
164
+ // Checking stored token
165
+ if (loading) {
166
+ return (
167
+ <div className="login-page">
168
+ <div className="loading-container">
169
+ <div className="loading-spinner" />
170
+ <p className="loading-text">Verifying session...</p>
171
+ </div>
172
+ </div>
173
+ );
174
+ }
175
+
176
+ // Not authenticated
177
+ if (!isAuthenticated) {
178
+ return <LoginPage />;
179
+ }
180
+
181
+ // Authenticated but template not yet loaded
182
+ if (!templateLoaded) {
183
+ return (
184
+ <div className="login-page">
185
+ <div className="loading-container">
186
+ <div className="loading-spinner" />
187
+ <p className="loading-text">
188
+ {templateError || 'Loading clinical template...'}
189
+ </p>
190
+ {templateError && (
191
+ <button
192
+ className="navbar__btn navbar__btn--primary"
193
+ onClick={() => { setTemplateError(''); setTemplateLoaded(false); }}
194
+ style={{ marginTop: 'var(--space-4)' }}
195
+ >
196
+ Retry
197
+ </button>
198
+ )}
199
+ </div>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ // Fully ready
205
+ return (
206
+ <FormProvider>
207
+ <AppContent templateSections={templateSections} />
208
+ </FormProvider>
209
+ );
210
+ }
211
+
212
+ export default function App() {
213
+ return (
214
+ <ThemeProvider>
215
+ <AuthProvider>
216
+ <AuthGate />
217
+ </AuthProvider>
218
+ </ThemeProvider>
219
+ );
220
+ }
frontend/src/components/ai/AIGeneratorPanel.jsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { X, Copy, Download, Sparkles } from 'lucide-react';
3
+ import { useForm } from '../../context/FormContext';
4
+ import { useAuth } from '../../context/AuthContext';
5
+ import toast from 'react-hot-toast';
6
+
7
+ const API_BASE = import.meta.env.VITE_API_URL || '';
8
+
9
+ export default function AIGeneratorPanel({ isOpen, onClose }) {
10
+ const { buildPayload } = useForm();
11
+ const { getAuthHeaders } = useAuth();
12
+ const [loading, setLoading] = useState(false);
13
+ const [generatedNote, setGeneratedNote] = useState('');
14
+ const [error, setError] = useState('');
15
+ const [usage, setUsage] = useState(null);
16
+
17
+ const handleGenerate = async () => {
18
+ setLoading(true);
19
+ setError('');
20
+ setGeneratedNote('');
21
+
22
+ try {
23
+ const payload = buildPayload();
24
+ const response = await fetch(`${API_BASE}/api/generate`, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
27
+ body: JSON.stringify(payload),
28
+ });
29
+
30
+ if (!response.ok) {
31
+ const err = await response.json().catch(() => ({}));
32
+ throw new Error(err.detail || `Server error: ${response.status}`);
33
+ }
34
+
35
+ const data = await response.json();
36
+ if (data.success) {
37
+ setGeneratedNote(data.note);
38
+ setUsage(data.usage);
39
+ } else {
40
+ throw new Error('Generation failed');
41
+ }
42
+ } catch (err) {
43
+ setError(err.message);
44
+ toast.error(err.message);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ const handleCopy = () => {
51
+ navigator.clipboard.writeText(generatedNote).then(() => {
52
+ toast.success('Note copied to clipboard');
53
+ });
54
+ };
55
+
56
+ const handleDownload = () => {
57
+ const blob = new Blob([generatedNote], { type: 'text/plain' });
58
+ const url = URL.createObjectURL(blob);
59
+ const a = document.createElement('a');
60
+ a.href = url;
61
+ a.download = `OT_Note_${new Date().toISOString().slice(0, 10)}.txt`;
62
+ a.click();
63
+ URL.revokeObjectURL(url);
64
+ toast.success('Note downloaded');
65
+ };
66
+
67
+ if (!isOpen) return null;
68
+
69
+ return (
70
+ <div className="preview-overlay" onClick={onClose}>
71
+ <div className="preview-panel" onClick={e => e.stopPropagation()}>
72
+ <div className="preview-panel__header">
73
+ <h3 className="preview-panel__title">
74
+ <Sparkles size={20} style={{ marginRight: 8, color: 'var(--accent-500)' }} />
75
+ AI Note Generator
76
+ </h3>
77
+ <button className="preview-panel__close" onClick={onClose}>
78
+ <X size={16} />
79
+ </button>
80
+ </div>
81
+
82
+ <div className="preview-panel__body">
83
+ {!generatedNote && !loading && !error && (
84
+ <div className="loading-container">
85
+ <p style={{ color: 'var(--text-secondary)', marginBottom: 'var(--space-4)', textAlign: 'center', maxWidth: 400 }}>
86
+ Click "Generate" to convert your selections into a polished, Med A-compliant clinical narrative using AI.
87
+ </p>
88
+ <button
89
+ className="navbar__btn navbar__btn--primary"
90
+ onClick={handleGenerate}
91
+ style={{ padding: 'var(--space-3) var(--space-8)', fontSize: 'var(--font-md)' }}
92
+ >
93
+ <Sparkles size={18} />
94
+ Generate Clinical Note
95
+ </button>
96
+ </div>
97
+ )}
98
+
99
+ {loading && (
100
+ <div className="loading-container">
101
+ <div className="loading-spinner" />
102
+ <p className="loading-text">Generating your clinical note...</p>
103
+ <p style={{ color: 'var(--text-tertiary)', fontSize: 'var(--font-xs)' }}>
104
+ This may take 5-15 seconds
105
+ </p>
106
+ </div>
107
+ )}
108
+
109
+ {error && (
110
+ <div className="loading-container">
111
+ <p style={{ color: 'var(--danger)', fontWeight: 600 }}>{error}</p>
112
+ <button
113
+ className="navbar__btn navbar__btn--primary"
114
+ onClick={handleGenerate}
115
+ style={{ marginTop: 'var(--space-4)' }}
116
+ >
117
+ Try Again
118
+ </button>
119
+ </div>
120
+ )}
121
+
122
+ {generatedNote && (
123
+ <div className="preview-panel__content">
124
+ {generatedNote.split('\n').map((line, i) => (
125
+ <p key={i} style={{ minHeight: line.trim() ? undefined : '0.5em' }}>
126
+ {line || '\u00A0'}
127
+ </p>
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+
133
+ {generatedNote && (
134
+ <div className="preview-panel__footer">
135
+ <button className="navbar__btn" onClick={handleCopy}>
136
+ <Copy size={16} />
137
+ Copy to Clipboard
138
+ </button>
139
+ <button className="navbar__btn" onClick={handleDownload}>
140
+ <Download size={16} />
141
+ Download .txt
142
+ </button>
143
+ <button className="navbar__btn navbar__btn--primary" onClick={handleGenerate}>
144
+ <Sparkles size={16} />
145
+ Regenerate
146
+ </button>
147
+ {usage && (
148
+ <span style={{ fontSize: 'var(--font-xs)', color: 'var(--text-tertiary)', marginLeft: 'auto' }}>
149
+ {usage.completion_tokens} tokens
150
+ </span>
151
+ )}
152
+ </div>
153
+ )}
154
+ </div>
155
+ </div>
156
+ );
157
+ }
frontend/src/components/auth/LoginPage.jsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useAuth } from '../../context/AuthContext';
3
+ import { Lock, User, Eye, EyeOff, Sparkles, AlertCircle } from 'lucide-react';
4
+
5
+ export default function LoginPage() {
6
+ const { login } = useAuth();
7
+ const [username, setUsername] = useState('');
8
+ const [password, setPassword] = useState('');
9
+ const [showPassword, setShowPassword] = useState(false);
10
+ const [error, setError] = useState('');
11
+ const [loading, setLoading] = useState(false);
12
+
13
+ const handleSubmit = async (e) => {
14
+ e.preventDefault();
15
+ if (!username.trim() || !password.trim()) {
16
+ setError('Please enter both username and password');
17
+ return;
18
+ }
19
+
20
+ setLoading(true);
21
+ setError('');
22
+
23
+ try {
24
+ await login(username.trim(), password);
25
+ } catch (err) {
26
+ setError(err.message || 'Invalid credentials');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="login-page">
34
+ <div className="login-card">
35
+ <div className="login-card__header">
36
+ <div className="login-card__icon">
37
+ <Sparkles size={28} />
38
+ </div>
39
+ <h1 className="login-card__title">
40
+ OT <span className="login-card__title-accent">NoteBuilder</span>
41
+ </h1>
42
+ <p className="login-card__subtitle">
43
+ Authorized personnel only
44
+ </p>
45
+ </div>
46
+
47
+ <form className="login-card__form" onSubmit={handleSubmit}>
48
+ {error && (
49
+ <div className="login-card__error">
50
+ <AlertCircle size={16} />
51
+ <span>{error}</span>
52
+ </div>
53
+ )}
54
+
55
+ <div className="login-card__field">
56
+ <label className="login-card__label" htmlFor="login-username">
57
+ Username
58
+ </label>
59
+ <div className="login-card__input-wrap">
60
+ <User size={16} className="login-card__input-icon" />
61
+ <input
62
+ id="login-username"
63
+ type="text"
64
+ className="login-card__input"
65
+ placeholder="Enter your username"
66
+ value={username}
67
+ onChange={e => setUsername(e.target.value)}
68
+ autoComplete="username"
69
+ autoFocus
70
+ disabled={loading}
71
+ />
72
+ </div>
73
+ </div>
74
+
75
+ <div className="login-card__field">
76
+ <label className="login-card__label" htmlFor="login-password">
77
+ Password
78
+ </label>
79
+ <div className="login-card__input-wrap">
80
+ <Lock size={16} className="login-card__input-icon" />
81
+ <input
82
+ id="login-password"
83
+ type={showPassword ? 'text' : 'password'}
84
+ className="login-card__input"
85
+ placeholder="Enter your password"
86
+ value={password}
87
+ onChange={e => setPassword(e.target.value)}
88
+ autoComplete="current-password"
89
+ disabled={loading}
90
+ />
91
+ <button
92
+ type="button"
93
+ className="login-card__eye"
94
+ onClick={() => setShowPassword(!showPassword)}
95
+ tabIndex={-1}
96
+ >
97
+ {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
98
+ </button>
99
+ </div>
100
+ </div>
101
+
102
+ <button
103
+ type="submit"
104
+ className="login-card__submit"
105
+ disabled={loading}
106
+ >
107
+ {loading ? (
108
+ <span className="login-card__spinner" />
109
+ ) : (
110
+ <>
111
+ <Lock size={16} />
112
+ Sign In
113
+ </>
114
+ )}
115
+ </button>
116
+ </form>
117
+
118
+
119
+ </div>
120
+ </div>
121
+ );
122
+ }
frontend/src/components/fields/CheckboxGroup.jsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useForm } from '../../context/FormContext';
3
+ import { Check, Plus } from 'lucide-react';
4
+
5
+ export default function CheckboxGroup({ field, sectionId }) {
6
+ const { formState, toggleCheckbox, addCustomCheckboxItem } = useForm();
7
+ const [customInput, setCustomInput] = useState('');
8
+
9
+ const fieldState = formState.sections[sectionId]?.[field.id] || { checked: [], customItems: [] };
10
+ const checked = fieldState.checked || [];
11
+ const allItems = [...(field.options || []), ...(fieldState.customItems || [])];
12
+
13
+ const handleAddCustom = () => {
14
+ if (customInput.trim()) {
15
+ addCustomCheckboxItem(sectionId, field.id, customInput.trim());
16
+ setCustomInput('');
17
+ }
18
+ };
19
+
20
+ return (
21
+ <div className="checkbox-group" id={`field-${field.id}`}>
22
+ <div className="checkbox-group__label">{field.label}</div>
23
+
24
+ {field.sentence && (
25
+ <div className="checkbox-group__sentence">{field.sentence}</div>
26
+ )}
27
+
28
+ <div className="checkbox-group__items">
29
+ {allItems.map(item => {
30
+ const isChecked = checked.includes(item);
31
+ return (
32
+ <div
33
+ key={item}
34
+ className={`checkbox-item ${isChecked ? 'checkbox-item--checked' : ''}`}
35
+ onClick={() => toggleCheckbox(sectionId, field.id, item)}
36
+ >
37
+ <div className="checkbox-item__box">
38
+ {isChecked && <Check size={12} strokeWidth={3} />}
39
+ </div>
40
+ <span className="checkbox-item__label">{item}</span>
41
+ </div>
42
+ );
43
+ })}
44
+ </div>
45
+
46
+ <div className="fill-blank__custom" style={{ marginTop: '0.5rem' }}>
47
+ <input
48
+ type="text"
49
+ className="fill-blank__custom-input"
50
+ placeholder="Add custom item..."
51
+ value={customInput}
52
+ onChange={e => setCustomInput(e.target.value)}
53
+ onKeyDown={e => e.key === 'Enter' && handleAddCustom()}
54
+ />
55
+ <button
56
+ type="button"
57
+ className="fill-blank__custom-add"
58
+ onClick={handleAddCustom}
59
+ title="Add custom checkbox item"
60
+ >
61
+ <Plus size={14} />
62
+ </button>
63
+ </div>
64
+ </div>
65
+ );
66
+ }
frontend/src/components/fields/ExerciseList.jsx ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useForm } from '../../context/FormContext';
2
+ import { Check } from 'lucide-react';
3
+
4
+ export default function ExerciseList({ field, sectionId }) {
5
+ const { formState, toggleExercise, updateExerciseValue } = useForm();
6
+
7
+ const fieldState = formState.sections[sectionId]?.[field.id] || { exercises: {} };
8
+ const exercises = fieldState.exercises || {};
9
+
10
+ return (
11
+ <div className="exercise-list" id={`field-${field.id}`}>
12
+ <div className="exercise-list__label">{field.label}</div>
13
+
14
+ {field.exercises.map(ex => {
15
+ const exState = exercises[ex.id] || { enabled: false, sets: '', reps: '', minutes: '', seconds: '', trials: '', steps: '' };
16
+ const isActive = exState.enabled;
17
+
18
+ return (
19
+ <div
20
+ key={ex.id}
21
+ className={`exercise-item ${isActive ? 'exercise-item--active' : ''}`}
22
+ >
23
+ <div
24
+ className="exercise-item__toggle"
25
+ onClick={() => toggleExercise(sectionId, field.id, ex.id)}
26
+ >
27
+ {isActive && <Check size={12} strokeWidth={3} />}
28
+ </div>
29
+
30
+ <span
31
+ className="exercise-item__name"
32
+ onClick={() => toggleExercise(sectionId, field.id, ex.id)}
33
+ >
34
+ {ex.name}
35
+ </span>
36
+
37
+ {isActive && (
38
+ <div className="exercise-item__inputs">
39
+ {ex.usesMinutes ? (
40
+ <>
41
+ <input
42
+ type="number"
43
+ className="exercise-item__input"
44
+ placeholder="min"
45
+ value={exState.minutes}
46
+ onChange={e => updateExerciseValue(sectionId, field.id, ex.id, 'minutes', e.target.value)}
47
+ min="0"
48
+ />
49
+ <span className="exercise-item__unit">min</span>
50
+ </>
51
+ ) : ex.usesSeconds ? (
52
+ <>
53
+ <input
54
+ type="number"
55
+ className="exercise-item__input"
56
+ placeholder="sec"
57
+ value={exState.seconds}
58
+ onChange={e => updateExerciseValue(sectionId, field.id, ex.id, 'seconds', e.target.value)}
59
+ min="0"
60
+ />
61
+ <span className="exercise-item__unit">sec</span>
62
+ <span className="exercise-item__separator">&times;</span>
63
+ <input
64
+ type="number"
65
+ className="exercise-item__input"
66
+ placeholder="reps"
67
+ value={exState.reps}
68
+ onChange={e => updateExerciseValue(sectionId, field.id, ex.id, 'reps', e.target.value)}
69
+ min="0"
70
+ />
71
+ <span className="exercise-item__unit">reps</span>
72
+ </>
73
+ ) : ex.usesTrials ? (
74
+ <>
75
+ <input
76
+ type="number"
77
+ className="exercise-item__input"
78
+ placeholder="#"
79
+ value={exState.trials}
80
+ onChange={e => updateExerciseValue(sectionId, field.id, ex.id, 'trials', e.target.value)}
81
+ min="0"
82
+ />
83
+ <span className="exercise-item__unit">{ex.usesSteps ? 'steps' : 'trials'}</span>
84
+ </>
85
+ ) : (
86
+ <>
87
+ <input
88
+ type="number"
89
+ className="exercise-item__input"
90
+ placeholder="sets"
91
+ value={exState.sets}
92
+ onChange={e => updateExerciseValue(sectionId, field.id, ex.id, 'sets', e.target.value)}
93
+ min="0"
94
+ />
95
+ <span className="exercise-item__separator">&times;</span>
96
+ <input
97
+ type="number"
98
+ className="exercise-item__input"
99
+ placeholder="reps"
100
+ value={exState.reps}
101
+ onChange={e => updateExerciseValue(sectionId, field.id, ex.id, 'reps', e.target.value)}
102
+ min="0"
103
+ />
104
+ <span className="exercise-item__unit">reps</span>
105
+ </>
106
+ )}
107
+ </div>
108
+ )}
109
+ </div>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ }
frontend/src/components/fields/FillBlank.jsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useForm } from '../../context/FormContext';
3
+ import { Plus } from 'lucide-react';
4
+
5
+ export default function FillBlank({ field, sectionId }) {
6
+ const { formState, toggleOption, addCustomOption } = useForm();
7
+ const [customInput, setCustomInput] = useState('');
8
+
9
+ const fieldState = formState.sections[sectionId]?.[field.id] || { selected: [], customOptions: [] };
10
+ const allOptions = [...(field.options || []), ...(fieldState.customOptions || [])];
11
+ const selected = fieldState.selected || [];
12
+
13
+ const filledSentence = field.sentence?.replace(
14
+ /____________/g,
15
+ selected.length > 0
16
+ ? selected.join(' / ')
17
+ : '____________'
18
+ );
19
+
20
+ const handleAddCustom = () => {
21
+ if (customInput.trim()) {
22
+ addCustomOption(sectionId, field.id, customInput.trim());
23
+ setCustomInput('');
24
+ }
25
+ };
26
+
27
+ const handleKeyDown = (e) => {
28
+ if (e.key === 'Enter') {
29
+ e.preventDefault();
30
+ handleAddCustom();
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div className="fill-blank" id={`field-${field.id}`}>
36
+ <div className="fill-blank__label">{field.label}</div>
37
+
38
+ {field.sentence && (
39
+ <div className="fill-blank__sentence">
40
+ {filledSentence.split('____________').map((part, i, arr) => (
41
+ <span key={i}>
42
+ {part}
43
+ {i < arr.length - 1 && (
44
+ <span className={`fill-blank__blank ${selected.length > 0 ? 'fill-blank__blank--filled' : ''}`}>
45
+ {selected.length > 0 ? selected.join(' / ') : '____________'}
46
+ </span>
47
+ )}
48
+ </span>
49
+ ))}
50
+ </div>
51
+ )}
52
+
53
+ <div className="fill-blank__options">
54
+ {allOptions.map(option => (
55
+ <button
56
+ key={option}
57
+ type="button"
58
+ className={`option-btn ${selected.includes(option) ? 'option-btn--selected' : ''}`}
59
+ onClick={() => toggleOption(sectionId, field.id, option, field.multiSelect)}
60
+ title={option}
61
+ >
62
+ {option}
63
+ </button>
64
+ ))}
65
+ </div>
66
+
67
+ <div className="fill-blank__custom">
68
+ <input
69
+ type="text"
70
+ className="fill-blank__custom-input"
71
+ placeholder="Add your own option..."
72
+ value={customInput}
73
+ onChange={e => setCustomInput(e.target.value)}
74
+ onKeyDown={handleKeyDown}
75
+ />
76
+ <button
77
+ type="button"
78
+ className="fill-blank__custom-add"
79
+ onClick={handleAddCustom}
80
+ title="Add custom option"
81
+ >
82
+ <Plus size={14} />
83
+ </button>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
frontend/src/components/fields/StaticText.jsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export default function StaticText({ field }) {
2
+ return (
3
+ <div className="static-text" id={`field-${field.id}`}>
4
+ <div className="static-text__content">{field.text}</div>
5
+ </div>
6
+ );
7
+ }
frontend/src/components/fields/TextInput.jsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useForm } from '../../context/FormContext';
2
+
3
+ export default function TextInput({ field, sectionId }) {
4
+ const { formState, updateTextField } = useForm();
5
+ const value = formState.sections[sectionId]?.[field.id] || '';
6
+
7
+ return (
8
+ <div className="text-input" id={`field-${field.id}`}>
9
+ <label className="text-input__label" htmlFor={`input-${field.id}`}>
10
+ {field.label}
11
+ </label>
12
+ <input
13
+ id={`input-${field.id}`}
14
+ type={field.type === 'number' ? 'number' : 'text'}
15
+ className="text-input__field"
16
+ placeholder={field.placeholder || ''}
17
+ value={value}
18
+ onChange={e => updateTextField(sectionId, field.id, e.target.value)}
19
+ min={field.type === 'number' ? '0' : undefined}
20
+ />
21
+ {field.sentence && (
22
+ <div className="text-input__sentence">{field.sentence}</div>
23
+ )}
24
+ </div>
25
+ );
26
+ }
frontend/src/components/layout/Navbar.jsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useTheme } from '../../context/ThemeContext';
2
+ import { useAuth } from '../../context/AuthContext';
3
+ import { Sun, Moon, Eye, Sparkles, LogOut, User, Menu, Settings } from 'lucide-react';
4
+
5
+ export default function Navbar({ onPreview, onGenerate, onToggleSidebar }) {
6
+ const { theme, toggleTheme } = useTheme();
7
+ const { user, logout } = useAuth();
8
+
9
+ return (
10
+ <header className="navbar" role="banner">
11
+ <div className="navbar__brand">
12
+ <button
13
+ className="navbar__hamburger"
14
+ onClick={onToggleSidebar}
15
+ title="Toggle sidebar"
16
+ aria-label="Toggle sidebar"
17
+ >
18
+ <Menu size={20} />
19
+ </button>
20
+ <div className="navbar__icon navbar__icon--desktop">
21
+ <Sparkles size={18} />
22
+ </div>
23
+ <h1 className="navbar__title">
24
+ OT <span className="navbar__title-accent">NoteBuilder</span>
25
+ </h1>
26
+ </div>
27
+
28
+ <div className="navbar__actions">
29
+ {/* User pill β€” hidden on small screens via CSS */}
30
+ <div className="navbar__user">
31
+ <User size={12} />
32
+ <span className="navbar__user-name">{user}</span>
33
+ </div>
34
+
35
+ <button
36
+ className="navbar__btn"
37
+ onClick={onPreview}
38
+ title="Preview filled template"
39
+ >
40
+ <Eye size={16} />
41
+ <span className="navbar__btn-label">Preview</span>
42
+ </button>
43
+
44
+ <button
45
+ className="navbar__btn navbar__btn--primary"
46
+ onClick={onGenerate}
47
+ title="Generate AI clinical note"
48
+ >
49
+ <Sparkles size={16} />
50
+ <span className="navbar__btn-label">Generate Note</span>
51
+ </button>
52
+
53
+ <button
54
+ className="theme-toggle"
55
+ onClick={toggleTheme}
56
+ title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
57
+ aria-label="Toggle theme"
58
+ >
59
+ {theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
60
+ </button>
61
+
62
+ <button
63
+ className="navbar__btn navbar__btn--logout"
64
+ onClick={logout}
65
+ title="Sign out"
66
+ >
67
+ <LogOut size={16} />
68
+ </button>
69
+ </div>
70
+ </header>
71
+ );
72
+ }
frontend/src/components/layout/Sidebar.jsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useForm } from '../../context/FormContext';
2
+ import { getTemplateData } from '../../data/templateData';
3
+ import * as Icons from 'lucide-react';
4
+ import { User, BookOpen, FileText, PlusCircle } from 'lucide-react';
5
+
6
+ function getIcon(name, size = 18) {
7
+ const Icon = Icons[name];
8
+ return Icon ? <Icon size={size} /> : null;
9
+ }
10
+
11
+ const SPECIAL_SECTIONS = [
12
+ { id: '_header', label: 'Patient Info', icon: <User size={18} /> },
13
+ { id: '_abbreviations', label: 'Abbreviations', icon: <BookOpen size={18} /> },
14
+ { id: '_smart_phrases', label: 'Smart Phrases', icon: <FileText size={18} /> },
15
+ ];
16
+
17
+ export default function Sidebar({ activeSection, onSectionClick, onAddSection, isOpen }) {
18
+ const { formState, getFilledCount } = useForm();
19
+ const TEMPLATE_SECTIONS = getTemplateData()?.sections || [];
20
+
21
+ return (
22
+ <nav className={`sidebar ${isOpen ? 'sidebar--open' : ''}`} aria-label="Section navigation">
23
+ <div className="sidebar__section-title">Document</div>
24
+
25
+ {SPECIAL_SECTIONS.map(s => (
26
+ <div
27
+ key={s.id}
28
+ className={`sidebar__item ${activeSection === s.id ? 'sidebar__item--active' : ''}`}
29
+ onClick={() => onSectionClick(s.id)}
30
+ >
31
+ <span className="sidebar__item-icon">{s.icon}</span>
32
+ <span className="sidebar__item-label">{s.label}</span>
33
+ </div>
34
+ ))}
35
+
36
+ <div className="sidebar__section-title">CPT Sections</div>
37
+
38
+ {TEMPLATE_SECTIONS.map(section => {
39
+ const count = getFilledCount(section.id);
40
+ const isEnabled = formState.enabledSections[section.id] !== false;
41
+ return (
42
+ <div
43
+ key={section.id}
44
+ className={`sidebar__item ${activeSection === section.id ? 'sidebar__item--active' : ''}`}
45
+ onClick={() => onSectionClick(section.id)}
46
+ style={{ opacity: isEnabled ? 1 : 0.4 }}
47
+ >
48
+ <span className="sidebar__item-icon">{getIcon(section.icon)}</span>
49
+ <span className="sidebar__item-label">
50
+ {section.cptCode ? `${section.cptCode} β€” ` : ''}
51
+ {section.title.replace(/^\d+\s*[—–-]\s*/, '')}
52
+ </span>
53
+ {count > 0 && <span className="sidebar__item-badge">{count}</span>}
54
+ </div>
55
+ );
56
+ })}
57
+
58
+ {formState.customSections.length > 0 && (
59
+ <>
60
+ <div className="sidebar__section-title">Custom Sections</div>
61
+ {formState.customSections.map(cs => (
62
+ <div
63
+ key={cs.id}
64
+ className={`sidebar__item ${activeSection === cs.id ? 'sidebar__item--active' : ''}`}
65
+ onClick={() => onSectionClick(cs.id)}
66
+ >
67
+ <span className="sidebar__item-icon"><FileText size={18} /></span>
68
+ <span className="sidebar__item-label">{cs.title}</span>
69
+ </div>
70
+ ))}
71
+ </>
72
+ )}
73
+
74
+ <div className="sidebar__item" onClick={onAddSection} style={{ color: 'var(--accent-500)', marginTop: 'var(--space-2)' }}>
75
+ <span className="sidebar__item-icon"><PlusCircle size={18} /></span>
76
+ <span className="sidebar__item-label">Add Custom Section</span>
77
+ </div>
78
+ </nav>
79
+ );
80
+ }
frontend/src/components/modals/AddSectionModal.jsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { X, Plus } from 'lucide-react';
3
+
4
+ export default function AddSectionModal({ isOpen, onClose, onAdd }) {
5
+ const [title, setTitle] = useState('');
6
+
7
+ if (!isOpen) return null;
8
+
9
+ const handleAdd = () => {
10
+ if (title.trim()) {
11
+ onAdd(title.trim());
12
+ setTitle('');
13
+ onClose();
14
+ }
15
+ };
16
+
17
+ return (
18
+ <div className="modal-overlay" onClick={onClose}>
19
+ <div className="modal" onClick={e => e.stopPropagation()}>
20
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-4)' }}>
21
+ <h3 className="modal__title" style={{ margin: 0 }}>Add Custom Section</h3>
22
+ <button className="preview-panel__close" onClick={onClose}>
23
+ <X size={16} />
24
+ </button>
25
+ </div>
26
+
27
+ <div className="text-input">
28
+ <label className="text-input__label" htmlFor="custom-section-title">
29
+ Section Title
30
+ </label>
31
+ <input
32
+ id="custom-section-title"
33
+ type="text"
34
+ className="text-input__field"
35
+ placeholder="e.g., Cognitive Retraining"
36
+ value={title}
37
+ onChange={e => setTitle(e.target.value)}
38
+ onKeyDown={e => e.key === 'Enter' && handleAdd()}
39
+ autoFocus
40
+ />
41
+ </div>
42
+
43
+ <div className="modal__actions">
44
+ <button className="navbar__btn" onClick={onClose}>
45
+ Cancel
46
+ </button>
47
+ <button
48
+ className="navbar__btn navbar__btn--primary"
49
+ onClick={handleAdd}
50
+ disabled={!title.trim()}
51
+ >
52
+ <Plus size={16} />
53
+ Add Section
54
+ </button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
frontend/src/components/preview/PreviewPanel.jsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { X, Copy, Download } from 'lucide-react';
2
+ import { useForm } from '../../context/FormContext';
3
+ import { getTemplateData } from '../../data/templateData';
4
+ import toast from 'react-hot-toast';
5
+
6
+ export default function PreviewPanel({ isOpen, onClose }) {
7
+ const { formState } = useForm();
8
+
9
+ if (!isOpen) return null;
10
+
11
+ const buildPreviewText = () => {
12
+ const TEMPLATE_SECTIONS = getTemplateData()?.sections || [];
13
+ const lines = [];
14
+
15
+ // Header
16
+ lines.push('SNF OCCUPATIONAL THERAPY \u2014 DAILY PROGRESS NOTE');
17
+ lines.push('='.repeat(50));
18
+ const pInfo = formState.patientInfo;
19
+ if (pInfo.pt_name) lines.push(`Pt: ${pInfo.pt_name}`);
20
+ if (pInfo.dos) lines.push(`DOS: ${pInfo.dos}`);
21
+ if (pInfo.dx) lines.push(`Dx: ${pInfo.dx}`);
22
+ if (pInfo.precautions) lines.push(`Precautions: ${pInfo.precautions}`);
23
+ if (pInfo.visit_number || pInfo.frequency) lines.push(`Visit #: ${pInfo.visit_number || '___'} Freq: ${pInfo.frequency || '___'}`);
24
+ if (pInfo.tx_time || pInfo.cpt_codes) lines.push(`Tx Time: ${pInfo.tx_time || '___'} min CPT: ${pInfo.cpt_codes || '___'}`);
25
+ lines.push('');
26
+
27
+ TEMPLATE_SECTIONS.forEach(section => {
28
+ if (formState.enabledSections[section.id] === false) return;
29
+ const sectionState = formState.sections[section.id];
30
+ if (!sectionState) return;
31
+
32
+ let hasContent = false;
33
+ const sectionLines = [`\n${'β€”'.repeat(50)}`, section.title, 'β€”'.repeat(50)];
34
+
35
+ section.subsections.forEach(sub => {
36
+ sub.fields.forEach(field => {
37
+ const val = sectionState[field.id];
38
+ if (!val) return;
39
+
40
+ if (field.type === 'fill-blank' && val.selected?.length > 0) {
41
+ hasContent = true;
42
+ let sentence = field.sentence || '';
43
+ sentence = sentence.replace('____________', val.selected.join(' / '));
44
+ sectionLines.push(` ${sentence}`);
45
+ } else if (field.type === 'checkbox-group' && val.checked?.length > 0) {
46
+ hasContent = true;
47
+ sectionLines.push(` ${field.label}:`);
48
+ val.checked.forEach(c => sectionLines.push(` \u2611 ${c}`));
49
+ } else if (field.type === 'exercise-list') {
50
+ const enabled = [];
51
+ Object.entries(val.exercises || {}).forEach(([exId, exState]) => {
52
+ if (exState.enabled) {
53
+ const ex = field.exercises.find(e => e.id === exId);
54
+ if (ex) {
55
+ let desc = ex.name;
56
+ if (exState.sets && exState.reps) desc += ` \u2014 ${exState.sets} \u00d7 ${exState.reps}`;
57
+ else if (exState.minutes) desc += ` \u2014 ${exState.minutes} min`;
58
+ else if (exState.seconds) desc += ` \u2014 ${exState.seconds} sec`;
59
+ else if (exState.trials) desc += ` \u2014 ${exState.trials} trials`;
60
+ enabled.push(desc);
61
+ }
62
+ }
63
+ });
64
+ if (enabled.length > 0) {
65
+ hasContent = true;
66
+ sectionLines.push(` ${field.label}: ${enabled.join(', ')}`);
67
+ }
68
+ } else if ((field.type === 'text' || field.type === 'number') && val) {
69
+ hasContent = true;
70
+ sectionLines.push(` ${field.label}: ${val}`);
71
+ }
72
+ });
73
+ });
74
+
75
+ if (hasContent) {
76
+ lines.push(...sectionLines);
77
+ }
78
+ });
79
+
80
+ return lines.join('\n');
81
+ };
82
+
83
+ const previewText = buildPreviewText();
84
+
85
+ const handleCopy = () => {
86
+ navigator.clipboard.writeText(previewText).then(() => {
87
+ toast.success('Preview copied to clipboard');
88
+ });
89
+ };
90
+
91
+ const handleDownload = () => {
92
+ const blob = new Blob([previewText], { type: 'text/plain' });
93
+ const url = URL.createObjectURL(blob);
94
+ const a = document.createElement('a');
95
+ a.href = url;
96
+ a.download = `OT_Preview_${new Date().toISOString().slice(0, 10)}.txt`;
97
+ a.click();
98
+ URL.revokeObjectURL(url);
99
+ };
100
+
101
+ return (
102
+ <div className="preview-overlay" onClick={onClose}>
103
+ <div className="preview-panel" onClick={e => e.stopPropagation()}>
104
+ <div className="preview-panel__header">
105
+ <h3 className="preview-panel__title">Template Preview</h3>
106
+ <button className="preview-panel__close" onClick={onClose}>
107
+ <X size={16} />
108
+ </button>
109
+ </div>
110
+ <div className="preview-panel__body">
111
+ <pre className="preview-panel__content" style={{ fontFamily: 'var(--font-family)', whiteSpace: 'pre-wrap' }}>
112
+ {previewText}
113
+ </pre>
114
+ </div>
115
+ <div className="preview-panel__footer">
116
+ <button className="navbar__btn" onClick={handleCopy}>
117
+ <Copy size={16} />
118
+ Copy
119
+ </button>
120
+ <button className="navbar__btn" onClick={handleDownload}>
121
+ <Download size={16} />
122
+ Download
123
+ </button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
frontend/src/components/reference/AbbreviationPanel.jsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getTemplateData } from '../../data/templateData';
2
+ import { BookOpen, ChevronDown } from 'lucide-react';
3
+ import { useState } from 'react';
4
+
5
+ export default function AbbreviationPanel() {
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ const ABBREVIATIONS = getTemplateData()?.abbreviations || [];
8
+
9
+ return (
10
+ <div className="section" id="section-abbreviations">
11
+ <div className="section__header" style={{ cursor: 'pointer' }} onClick={() => setIsOpen(!isOpen)}>
12
+ <div className="section__header-icon">
13
+ <BookOpen size={20} />
14
+ </div>
15
+ <h2 className="section__title">Net Health Abbreviation Quick Reference</h2>
16
+ <span className={`collapsible-trigger__icon ${isOpen ? 'collapsible-trigger__icon--open' : ''}`} style={{ marginLeft: 'auto' }}>
17
+ <ChevronDown size={20} />
18
+ </span>
19
+ </div>
20
+
21
+ {isOpen && (
22
+ <div className="abbrev-panel">
23
+ {ABBREVIATIONS.map(cat => (
24
+ <div className="abbrev-category" key={cat.category}>
25
+ <div className="abbrev-category__title">{cat.category}</div>
26
+ <div className="abbrev-list">
27
+ {cat.items.map(item => (
28
+ <span className="abbrev-chip" key={item.abbr}>
29
+ <span className="abbrev-chip__abbr">{item.abbr}</span>
30
+ <span className="abbrev-chip__meaning">= {item.meaning}</span>
31
+ </span>
32
+ ))}
33
+ </div>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ )}
38
+ </div>
39
+ );
40
+ }
frontend/src/components/reference/SmartPhrasesPanel.jsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getTemplateData } from '../../data/templateData';
2
+ import { FileText, Copy, ChevronDown } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import toast from 'react-hot-toast';
5
+
6
+ export default function SmartPhrasesPanel() {
7
+ const [isOpen, setIsOpen] = useState(false);
8
+ const SMART_PHRASES = getTemplateData()?.smartPhrases || [];
9
+
10
+ const handleCopy = (text) => {
11
+ navigator.clipboard.writeText(text).then(() => {
12
+ toast.success('Copied to clipboard');
13
+ });
14
+ };
15
+
16
+ return (
17
+ <div className="section" id="section-smart-phrases">
18
+ <div className="section__header" style={{ cursor: 'pointer' }} onClick={() => setIsOpen(!isOpen)}>
19
+ <div className="section__header-icon">
20
+ <FileText size={20} />
21
+ </div>
22
+ <h2 className="section__title">Quick Smart Phrases</h2>
23
+ <span className={`collapsible-trigger__icon ${isOpen ? 'collapsible-trigger__icon--open' : ''}`} style={{ marginLeft: 'auto' }}>
24
+ <ChevronDown size={20} />
25
+ </span>
26
+ </div>
27
+
28
+ {isOpen && (
29
+ <div className="smart-phrases__list">
30
+ {SMART_PHRASES.map((phrase, i) => (
31
+ <div
32
+ key={i}
33
+ className="smart-phrase"
34
+ onClick={() => handleCopy(phrase)}
35
+ title="Click to copy"
36
+ >
37
+ <span className="smart-phrase__number">{i + 1}.</span>
38
+ <span>{phrase}</span>
39
+ <span className="smart-phrase__copy">
40
+ <Copy size={14} />
41
+ </span>
42
+ </div>
43
+ ))}
44
+ </div>
45
+ )}
46
+ </div>
47
+ );
48
+ }
frontend/src/components/sections/PatientHeader.jsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useForm } from '../../context/FormContext';
2
+ import { getTemplateData } from '../../data/templateData';
3
+ import { User } from 'lucide-react';
4
+
5
+ export default function PatientHeader() {
6
+ const { formState, updatePatientInfo } = useForm();
7
+ const HEADER_FIELDS = getTemplateData()?.headerFields || [];
8
+ return (
9
+ <div className="section" id="section-patient-header">
10
+ <div className="section__header">
11
+ <div className="section__header-icon">
12
+ <User size={20} />
13
+ </div>
14
+ <h2 className="section__title">Patient Information</h2>
15
+ </div>
16
+
17
+ <div className="subsection">
18
+ <div className="subsection__title">
19
+ <span className="subsection__title-dot" />
20
+ Header Fields
21
+ </div>
22
+
23
+ <div className="patient-header">
24
+ {HEADER_FIELDS.map(field => (
25
+ <div className="text-input" key={field.id}>
26
+ <label className="text-input__label" htmlFor={`header-${field.id}`}>
27
+ {field.label}
28
+ </label>
29
+ <input
30
+ id={`header-${field.id}`}
31
+ type={field.type === 'number' ? 'number' : 'text'}
32
+ className="text-input__field"
33
+ placeholder={field.placeholder}
34
+ value={formState.patientInfo[field.id] || ''}
35
+ onChange={e => updatePatientInfo(field.id, e.target.value)}
36
+ />
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
frontend/src/components/sections/SectionRenderer.jsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useForm } from '../../context/FormContext';
3
+ import FillBlank from '../fields/FillBlank';
4
+ import CheckboxGroup from '../fields/CheckboxGroup';
5
+ import ExerciseList from '../fields/ExerciseList';
6
+ import TextInput from '../fields/TextInput';
7
+ import StaticText from '../fields/StaticText';
8
+ import { ChevronDown, ChevronRight } from 'lucide-react';
9
+ import * as Icons from 'lucide-react';
10
+
11
+ function getIcon(name, size = 20) {
12
+ const Icon = Icons[name];
13
+ return Icon ? <Icon size={size} /> : null;
14
+ }
15
+
16
+ function FieldRenderer({ field, sectionId }) {
17
+ switch (field.type) {
18
+ case 'fill-blank':
19
+ return <FillBlank field={field} sectionId={sectionId} />;
20
+ case 'checkbox-group':
21
+ return <CheckboxGroup field={field} sectionId={sectionId} />;
22
+ case 'exercise-list':
23
+ return <ExerciseList field={field} sectionId={sectionId} />;
24
+ case 'text':
25
+ case 'number':
26
+ return <TextInput field={field} sectionId={sectionId} />;
27
+ case 'static':
28
+ return <StaticText field={field} />;
29
+ default:
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export default function SectionRenderer({ section }) {
35
+ const { formState, toggleSection } = useForm();
36
+ const isEnabled = formState.enabledSections[section.id] !== false;
37
+ const [collapsedSubs, setCollapsedSubs] = useState({});
38
+
39
+ const toggleSubsection = (subId) => {
40
+ setCollapsedSubs(prev => ({ ...prev, [subId]: !prev[subId] }));
41
+ };
42
+
43
+ return (
44
+ <div className="section" id={`section-${section.id}`}>
45
+ <div className="section__header">
46
+ <div className="section__header-icon">
47
+ {getIcon(section.icon, 20)}
48
+ </div>
49
+ <h2 className="section__title">{section.title}</h2>
50
+ {section.cptCode && (
51
+ <span className="section__cpt">{section.cptCode}</span>
52
+ )}
53
+ <div className="section__toggle">
54
+ <div
55
+ className={`toggle-switch ${isEnabled ? 'toggle-switch--active' : ''}`}
56
+ onClick={() => toggleSection(section.id)}
57
+ role="switch"
58
+ aria-checked={isEnabled}
59
+ title={isEnabled ? 'Disable section' : 'Enable section'}
60
+ />
61
+ </div>
62
+ </div>
63
+
64
+ <div className={!isEnabled ? 'section-disabled' : ''}>
65
+ {section.subsections.map(sub => {
66
+ const isCollapsed = collapsedSubs[sub.id];
67
+ return (
68
+ <div className="subsection" key={sub.id} id={`subsection-${sub.id}`}>
69
+ <button
70
+ className="collapsible-trigger"
71
+ onClick={() => toggleSubsection(sub.id)}
72
+ type="button"
73
+ >
74
+ <span className="subsection__title" style={{ margin: 0 }}>
75
+ <span className="subsection__title-dot" />
76
+ {sub.title}
77
+ </span>
78
+ <span className={`collapsible-trigger__icon ${!isCollapsed ? 'collapsible-trigger__icon--open' : ''}`}>
79
+ <ChevronDown size={16} />
80
+ </span>
81
+ </button>
82
+
83
+ {!isCollapsed && (
84
+ <div style={{ paddingTop: 'var(--space-3)' }}>
85
+ {sub.fields.map(field => (
86
+ <FieldRenderer
87
+ key={field.id}
88
+ field={field}
89
+ sectionId={section.id}
90
+ />
91
+ ))}
92
+ </div>
93
+ )}
94
+ </div>
95
+ );
96
+ })}
97
+ </div>
98
+ </div>
99
+ );
100
+ }
frontend/src/context/AuthContext.jsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext, useState, useEffect, useCallback } from 'react';
2
+
3
+ const AuthContext = createContext();
4
+
5
+ const API_BASE = import.meta.env.VITE_API_URL || '';
6
+
7
+ export function AuthProvider({ children }) {
8
+ const [user, setUser] = useState(null);
9
+ const [token, setToken] = useState(null);
10
+ const [loading, setLoading] = useState(true);
11
+
12
+ // On mount, check if we have a stored token and verify it
13
+ useEffect(() => {
14
+ const storedToken = sessionStorage.getItem('ot-token');
15
+ const storedUser = sessionStorage.getItem('ot-user');
16
+
17
+ if (storedToken && storedUser) {
18
+ // Verify the token is still valid
19
+ fetch(`${API_BASE}/api/verify`, {
20
+ headers: { Authorization: `Bearer ${storedToken}` },
21
+ })
22
+ .then(res => {
23
+ if (res.ok) {
24
+ setToken(storedToken);
25
+ setUser(storedUser);
26
+ } else {
27
+ // Token invalid/expired β€” clear storage
28
+ sessionStorage.removeItem('ot-token');
29
+ sessionStorage.removeItem('ot-user');
30
+ }
31
+ })
32
+ .catch(() => {
33
+ sessionStorage.removeItem('ot-token');
34
+ sessionStorage.removeItem('ot-user');
35
+ })
36
+ .finally(() => setLoading(false));
37
+ } else {
38
+ setLoading(false);
39
+ }
40
+ }, []);
41
+
42
+ const login = useCallback(async (username, password) => {
43
+ const res = await fetch(`${API_BASE}/api/login`, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({ username, password }),
47
+ });
48
+
49
+ if (!res.ok) {
50
+ const err = await res.json().catch(() => ({}));
51
+ throw new Error(err.detail || 'Login failed');
52
+ }
53
+
54
+ const data = await res.json();
55
+ setToken(data.token);
56
+ setUser(data.username);
57
+ sessionStorage.setItem('ot-token', data.token);
58
+ sessionStorage.setItem('ot-user', data.username);
59
+ return data;
60
+ }, []);
61
+
62
+ const logout = useCallback(() => {
63
+ setToken(null);
64
+ setUser(null);
65
+ sessionStorage.removeItem('ot-token');
66
+ sessionStorage.removeItem('ot-user');
67
+ }, []);
68
+
69
+ const getAuthHeaders = useCallback(() => {
70
+ if (!token) return {};
71
+ return { Authorization: `Bearer ${token}` };
72
+ }, [token]);
73
+
74
+ const isAuthenticated = !!token && !!user;
75
+
76
+ return (
77
+ <AuthContext.Provider value={{ user, token, loading, isAuthenticated, login, logout, getAuthHeaders }}>
78
+ {children}
79
+ </AuthContext.Provider>
80
+ );
81
+ }
82
+
83
+ export function useAuth() {
84
+ const ctx = useContext(AuthContext);
85
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider');
86
+ return ctx;
87
+ }
frontend/src/context/FormContext.jsx ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext, useState, useCallback } from 'react';
2
+ import { getTemplateData } from '../data/templateData';
3
+
4
+ const FormContext = createContext();
5
+
6
+ function initializeFormState() {
7
+ const data = getTemplateData();
8
+ const TEMPLATE_SECTIONS = data?.sections || [];
9
+ const HEADER_FIELDS = data?.headerFields || [];
10
+
11
+ const state = {
12
+ patientInfo: {},
13
+ sections: {},
14
+ customSections: [],
15
+ enabledSections: {},
16
+ };
17
+
18
+ // Initialize header fields
19
+ HEADER_FIELDS.forEach(f => {
20
+ state.patientInfo[f.id] = '';
21
+ });
22
+
23
+ // Initialize all sections
24
+ TEMPLATE_SECTIONS.forEach(section => {
25
+ state.enabledSections[section.id] = true;
26
+ state.sections[section.id] = {};
27
+
28
+ section.subsections.forEach(sub => {
29
+ sub.fields.forEach(field => {
30
+ if (field.type === 'fill-blank') {
31
+ state.sections[section.id][field.id] = {
32
+ selected: [],
33
+ customOptions: [],
34
+ };
35
+ } else if (field.type === 'checkbox-group') {
36
+ state.sections[section.id][field.id] = {
37
+ checked: [],
38
+ customItems: [],
39
+ };
40
+ } else if (field.type === 'exercise-list') {
41
+ const exercises = {};
42
+ field.exercises.forEach(ex => {
43
+ exercises[ex.id] = { enabled: false, sets: '', reps: '', minutes: '', seconds: '', trials: '', steps: '' };
44
+ });
45
+ state.sections[section.id][field.id] = { exercises, customExercises: [] };
46
+ } else if (field.type === 'text' || field.type === 'number') {
47
+ state.sections[section.id][field.id] = '';
48
+ }
49
+ // static type needs no state
50
+ });
51
+ });
52
+ });
53
+
54
+ return state;
55
+ }
56
+
57
+ export function FormProvider({ children }) {
58
+ const [formState, setFormState] = useState(initializeFormState);
59
+
60
+ // ── Patient info ──
61
+ const updatePatientInfo = useCallback((fieldId, value) => {
62
+ setFormState(prev => ({
63
+ ...prev,
64
+ patientInfo: { ...prev.patientInfo, [fieldId]: value },
65
+ }));
66
+ }, []);
67
+
68
+ // ── Toggle section ──
69
+ const toggleSection = useCallback((sectionId) => {
70
+ setFormState(prev => ({
71
+ ...prev,
72
+ enabledSections: {
73
+ ...prev.enabledSections,
74
+ [sectionId]: !prev.enabledSections[sectionId],
75
+ },
76
+ }));
77
+ }, []);
78
+
79
+ // ── Fill-blank selection ──
80
+ const toggleOption = useCallback((sectionId, fieldId, option, multiSelect) => {
81
+ setFormState(prev => {
82
+ const current = prev.sections[sectionId]?.[fieldId]?.selected || [];
83
+ let updated;
84
+ if (multiSelect) {
85
+ updated = current.includes(option)
86
+ ? current.filter(o => o !== option)
87
+ : [...current, option];
88
+ } else {
89
+ updated = current.includes(option) ? [] : [option];
90
+ }
91
+ return {
92
+ ...prev,
93
+ sections: {
94
+ ...prev.sections,
95
+ [sectionId]: {
96
+ ...prev.sections[sectionId],
97
+ [fieldId]: {
98
+ ...prev.sections[sectionId][fieldId],
99
+ selected: updated,
100
+ },
101
+ },
102
+ },
103
+ };
104
+ });
105
+ }, []);
106
+
107
+ // ── Add custom option to a fill-blank ──
108
+ const addCustomOption = useCallback((sectionId, fieldId, option) => {
109
+ if (!option.trim()) return;
110
+ setFormState(prev => {
111
+ const field = prev.sections[sectionId]?.[fieldId] || { selected: [], customOptions: [] };
112
+ if (field.customOptions.includes(option)) return prev;
113
+ return {
114
+ ...prev,
115
+ sections: {
116
+ ...prev.sections,
117
+ [sectionId]: {
118
+ ...prev.sections[sectionId],
119
+ [fieldId]: {
120
+ ...field,
121
+ customOptions: [...(field.customOptions || []), option],
122
+ selected: [...(field.selected || []), option],
123
+ },
124
+ },
125
+ },
126
+ };
127
+ });
128
+ }, []);
129
+
130
+ // ── Checkbox toggle ──
131
+ const toggleCheckbox = useCallback((sectionId, fieldId, item) => {
132
+ setFormState(prev => {
133
+ const current = prev.sections[sectionId]?.[fieldId]?.checked || [];
134
+ const updated = current.includes(item)
135
+ ? current.filter(i => i !== item)
136
+ : [...current, item];
137
+ return {
138
+ ...prev,
139
+ sections: {
140
+ ...prev.sections,
141
+ [sectionId]: {
142
+ ...prev.sections[sectionId],
143
+ [fieldId]: {
144
+ ...prev.sections[sectionId][fieldId],
145
+ checked: updated,
146
+ },
147
+ },
148
+ },
149
+ };
150
+ });
151
+ }, []);
152
+
153
+ // ── Add custom checkbox item ──
154
+ const addCustomCheckboxItem = useCallback((sectionId, fieldId, item) => {
155
+ if (!item.trim()) return;
156
+ setFormState(prev => {
157
+ const field = prev.sections[sectionId]?.[fieldId] || { checked: [], customItems: [] };
158
+ if (field.customItems?.includes(item)) return prev;
159
+ return {
160
+ ...prev,
161
+ sections: {
162
+ ...prev.sections,
163
+ [sectionId]: {
164
+ ...prev.sections[sectionId],
165
+ [fieldId]: {
166
+ ...field,
167
+ customItems: [...(field.customItems || []), item],
168
+ checked: [...(field.checked || []), item],
169
+ },
170
+ },
171
+ },
172
+ };
173
+ });
174
+ }, []);
175
+
176
+ // ── Exercise toggle & update ──
177
+ const toggleExercise = useCallback((sectionId, fieldId, exerciseId) => {
178
+ setFormState(prev => {
179
+ const exState = prev.sections[sectionId]?.[fieldId]?.exercises?.[exerciseId];
180
+ if (!exState) return prev;
181
+ return {
182
+ ...prev,
183
+ sections: {
184
+ ...prev.sections,
185
+ [sectionId]: {
186
+ ...prev.sections[sectionId],
187
+ [fieldId]: {
188
+ ...prev.sections[sectionId][fieldId],
189
+ exercises: {
190
+ ...prev.sections[sectionId][fieldId].exercises,
191
+ [exerciseId]: { ...exState, enabled: !exState.enabled },
192
+ },
193
+ },
194
+ },
195
+ },
196
+ };
197
+ });
198
+ }, []);
199
+
200
+ const updateExerciseValue = useCallback((sectionId, fieldId, exerciseId, key, value) => {
201
+ setFormState(prev => {
202
+ const exState = prev.sections[sectionId]?.[fieldId]?.exercises?.[exerciseId];
203
+ if (!exState) return prev;
204
+ return {
205
+ ...prev,
206
+ sections: {
207
+ ...prev.sections,
208
+ [sectionId]: {
209
+ ...prev.sections[sectionId],
210
+ [fieldId]: {
211
+ ...prev.sections[sectionId][fieldId],
212
+ exercises: {
213
+ ...prev.sections[sectionId][fieldId].exercises,
214
+ [exerciseId]: { ...exState, [key]: value },
215
+ },
216
+ },
217
+ },
218
+ },
219
+ };
220
+ });
221
+ }, []);
222
+
223
+ // ── Text / number update ──
224
+ const updateTextField = useCallback((sectionId, fieldId, value) => {
225
+ setFormState(prev => ({
226
+ ...prev,
227
+ sections: {
228
+ ...prev.sections,
229
+ [sectionId]: {
230
+ ...prev.sections[sectionId],
231
+ [fieldId]: value,
232
+ },
233
+ },
234
+ }));
235
+ }, []);
236
+
237
+ // ── Custom sections ──
238
+ const addCustomSection = useCallback((title) => {
239
+ const id = `custom_${Date.now()}`;
240
+ setFormState(prev => ({
241
+ ...prev,
242
+ customSections: [
243
+ ...prev.customSections,
244
+ {
245
+ id,
246
+ title,
247
+ fields: [],
248
+ },
249
+ ],
250
+ enabledSections: { ...prev.enabledSections, [id]: true },
251
+ }));
252
+ return id;
253
+ }, []);
254
+
255
+ const addFieldToCustomSection = useCallback((sectionId, field) => {
256
+ setFormState(prev => ({
257
+ ...prev,
258
+ customSections: prev.customSections.map(s =>
259
+ s.id === sectionId
260
+ ? { ...s, fields: [...s.fields, field] }
261
+ : s
262
+ ),
263
+ }));
264
+ }, []);
265
+
266
+ const removeCustomSection = useCallback((sectionId) => {
267
+ setFormState(prev => ({
268
+ ...prev,
269
+ customSections: prev.customSections.filter(s => s.id !== sectionId),
270
+ }));
271
+ }, []);
272
+
273
+ // ── Build payload for API ──
274
+ const buildPayload = useCallback(() => {
275
+ const data = getTemplateData();
276
+ const TEMPLATE_SECTIONS = data?.sections || [];
277
+
278
+ const payload = {
279
+ patientInfo: formState.patientInfo,
280
+ sections: [],
281
+ customSections: [],
282
+ };
283
+
284
+ TEMPLATE_SECTIONS.forEach(section => {
285
+ if (!formState.enabledSections[section.id]) return;
286
+
287
+ const sectionData = {
288
+ sectionId: section.id,
289
+ sectionTitle: section.title,
290
+ enabled: true,
291
+ fields: [],
292
+ };
293
+
294
+ section.subsections.forEach(sub => {
295
+ sub.fields.forEach(field => {
296
+ const val = formState.sections[section.id]?.[field.id];
297
+ if (!val) return;
298
+
299
+ if (field.type === 'fill-blank') {
300
+ if (val.selected?.length > 0) {
301
+ sectionData.fields.push({
302
+ fieldId: field.id,
303
+ fieldLabel: field.label,
304
+ selectedOptions: val.selected,
305
+ });
306
+ }
307
+ } else if (field.type === 'checkbox-group') {
308
+ if (val.checked?.length > 0) {
309
+ sectionData.fields.push({
310
+ fieldId: field.id,
311
+ fieldLabel: field.label,
312
+ selectedOptions: val.checked,
313
+ });
314
+ }
315
+ } else if (field.type === 'exercise-list') {
316
+ const enabledExercises = [];
317
+ Object.entries(val.exercises || {}).forEach(([exId, exState]) => {
318
+ if (exState.enabled) {
319
+ const ex = field.exercises.find(e => e.id === exId);
320
+ if (ex) {
321
+ let desc = ex.name;
322
+ if (exState.sets && exState.reps) desc += ` \u2014 ${exState.sets} \u00d7 ${exState.reps} reps`;
323
+ else if (exState.minutes) desc += ` \u2014 ${exState.minutes} min`;
324
+ else if (exState.seconds) desc += ` \u2014 ${exState.seconds} sec`;
325
+ else if (exState.trials) desc += ` \u2014 ${exState.trials} trials`;
326
+ else if (exState.steps) desc += ` \u2014 ${exState.steps} steps`;
327
+ enabledExercises.push(desc);
328
+ }
329
+ }
330
+ });
331
+ if (enabledExercises.length > 0) {
332
+ sectionData.fields.push({
333
+ fieldId: field.id,
334
+ fieldLabel: field.label,
335
+ selectedOptions: enabledExercises,
336
+ });
337
+ }
338
+ } else if ((field.type === 'text' || field.type === 'number') && val) {
339
+ sectionData.fields.push({
340
+ fieldId: field.id,
341
+ fieldLabel: field.label,
342
+ selectedOptions: [],
343
+ customText: String(val),
344
+ });
345
+ }
346
+ });
347
+ });
348
+
349
+ if (sectionData.fields.length > 0) {
350
+ payload.sections.push(sectionData);
351
+ }
352
+ });
353
+
354
+ return payload;
355
+ }, [formState]);
356
+
357
+ // ── Count filled fields per section ──
358
+ const getFilledCount = useCallback((sectionId) => {
359
+ const sectionState = formState.sections[sectionId];
360
+ if (!sectionState) return 0;
361
+ let count = 0;
362
+ Object.values(sectionState).forEach(val => {
363
+ if (typeof val === 'string' && val) count++;
364
+ else if (val?.selected?.length > 0) count++;
365
+ else if (val?.checked?.length > 0) count++;
366
+ else if (val?.exercises) {
367
+ const hasEnabled = Object.values(val.exercises).some(e => e.enabled);
368
+ if (hasEnabled) count++;
369
+ }
370
+ });
371
+ return count;
372
+ }, [formState]);
373
+
374
+ const value = {
375
+ formState,
376
+ updatePatientInfo,
377
+ toggleSection,
378
+ toggleOption,
379
+ addCustomOption,
380
+ toggleCheckbox,
381
+ addCustomCheckboxItem,
382
+ toggleExercise,
383
+ updateExerciseValue,
384
+ updateTextField,
385
+ addCustomSection,
386
+ addFieldToCustomSection,
387
+ removeCustomSection,
388
+ buildPayload,
389
+ getFilledCount,
390
+ };
391
+
392
+ return (
393
+ <FormContext.Provider value={value}>
394
+ {children}
395
+ </FormContext.Provider>
396
+ );
397
+ }
398
+
399
+ export function useForm() {
400
+ const ctx = useContext(FormContext);
401
+ if (!ctx) throw new Error('useForm must be used within FormProvider');
402
+ return ctx;
403
+ }
frontend/src/context/ThemeContext.jsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext, useState, useEffect, useCallback } from 'react';
2
+
3
+ const ThemeContext = createContext();
4
+
5
+ export function ThemeProvider({ children }) {
6
+ const [theme, setTheme] = useState(() => {
7
+ const saved = localStorage.getItem('ot-theme');
8
+ return saved || 'dark';
9
+ });
10
+
11
+ useEffect(() => {
12
+ document.documentElement.setAttribute('data-theme', theme);
13
+ localStorage.setItem('ot-theme', theme);
14
+ }, [theme]);
15
+
16
+ const toggleTheme = useCallback(() => {
17
+ // 1. Kill ALL transitions so every element swaps color instantly
18
+ document.documentElement.classList.add('theme-switching');
19
+
20
+ // 2. Swap theme immediately
21
+ setTheme(prev => (prev === 'dark' ? 'light' : 'dark'));
22
+
23
+ // 3. Force reflow so the new colors paint before transitions re-enable
24
+ // eslint-disable-next-line no-unused-expressions
25
+ document.documentElement.offsetHeight;
26
+
27
+ // 4. Re-enable transitions after a single frame
28
+ requestAnimationFrame(() => {
29
+ requestAnimationFrame(() => {
30
+ document.documentElement.classList.remove('theme-switching');
31
+ });
32
+ });
33
+ }, []);
34
+
35
+ return (
36
+ <ThemeContext.Provider value={{ theme, toggleTheme }}>
37
+ {children}
38
+ </ThemeContext.Provider>
39
+ );
40
+ }
41
+
42
+ export function useTheme() {
43
+ const ctx = useContext(ThemeContext);
44
+ if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
45
+ return ctx;
46
+ }
frontend/src/data/templateData.js ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Template Data Loader
3
+ * ====================
4
+ * Fetches the template structure from the protected backend API.
5
+ * This file replaces the original templateData.js β€” the actual data
6
+ * lives on the server and is only accessible to authenticated users.
7
+ */
8
+
9
+ const API_BASE = import.meta.env.VITE_API_URL || '';
10
+
11
+ // In-memory cache so we only fetch once per session
12
+ let _cachedData = null;
13
+
14
+ /**
15
+ * Fetches the complete template data from the backend.
16
+ * Must be called after authentication (needs Bearer token).
17
+ */
18
+ export async function fetchTemplateData(token) {
19
+ if (_cachedData) return _cachedData;
20
+
21
+ const res = await fetch(`${API_BASE}/api/template`, {
22
+ headers: { Authorization: `Bearer ${token}` },
23
+ });
24
+
25
+ if (!res.ok) {
26
+ throw new Error('Failed to load template data');
27
+ }
28
+
29
+ _cachedData = await res.json();
30
+ return _cachedData;
31
+ }
32
+
33
+ /**
34
+ * Returns cached template data (must call fetchTemplateData first).
35
+ */
36
+ export function getTemplateData() {
37
+ return _cachedData;
38
+ }
39
+
40
+ // Provide empty defaults for imports that happen before fetch completes.
41
+ // These get replaced once fetchTemplateData() resolves.
42
+ export let ABBREVIATIONS = [];
43
+ export let SMART_PHRASES = [];
44
+ export let HEADER_FIELDS = [];
45
+
46
+ // Default export β€” the sections array
47
+ let TEMPLATE_SECTIONS = [];
48
+ export default TEMPLATE_SECTIONS;
49
+
50
+ /**
51
+ * Called after fetchTemplateData resolves to populate the module-level exports.
52
+ */
53
+ export function hydrateTemplateData(data) {
54
+ TEMPLATE_SECTIONS = data.sections || [];
55
+ ABBREVIATIONS = data.abbreviations || [];
56
+ SMART_PHRASES = data.smartPhrases || [];
57
+ HEADER_FIELDS = data.headerFields || [];
58
+ _cachedData = data;
59
+
60
+ // Return for convenience
61
+ return { TEMPLATE_SECTIONS, ABBREVIATIONS, SMART_PHRASES, HEADER_FIELDS };
62
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,1961 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ═══════════════════════════════════════════════════════════
2
+ OT Note Builder β€” Design System
3
+ ═══════════════════════════════════════════════════════════ */
4
+
5
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
6
+
7
+ /* ── CSS Custom Properties (Design Tokens) ── */
8
+
9
+ :root {
10
+ /* Green accent palette */
11
+ --accent-50: #ecfdf5;
12
+ --accent-100: #d1fae5;
13
+ --accent-200: #a7f3d0;
14
+ --accent-300: #6ee7b7;
15
+ --accent-400: #34d399;
16
+ --accent-500: #10b981;
17
+ --accent-600: #059669;
18
+ --accent-700: #047857;
19
+ --accent-800: #065f46;
20
+ --accent-900: #064e3b;
21
+
22
+ /* Neutrals */
23
+ --neutral-50: #f9fafb;
24
+ --neutral-100: #f3f4f6;
25
+ --neutral-200: #e5e7eb;
26
+ --neutral-300: #d1d5db;
27
+ --neutral-400: #9ca3af;
28
+ --neutral-500: #6b7280;
29
+ --neutral-600: #4b5563;
30
+ --neutral-700: #374151;
31
+ --neutral-800: #1f2937;
32
+ --neutral-900: #111827;
33
+ --neutral-950: #030712;
34
+
35
+ /* Semantic */
36
+ --danger: #ef4444;
37
+ --warning: #f59e0b;
38
+ --info: #3b82f6;
39
+
40
+ /* Typography */
41
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
42
+ --font-xs: 0.75rem;
43
+ --font-sm: 0.8125rem;
44
+ --font-base: 0.875rem;
45
+ --font-md: 1rem;
46
+ --font-lg: 1.125rem;
47
+ --font-xl: 1.25rem;
48
+ --font-2xl: 1.5rem;
49
+ --font-3xl: 1.875rem;
50
+
51
+ /* Spacing */
52
+ --space-1: 0.25rem;
53
+ --space-2: 0.5rem;
54
+ --space-3: 0.75rem;
55
+ --space-4: 1rem;
56
+ --space-5: 1.25rem;
57
+ --space-6: 1.5rem;
58
+ --space-8: 2rem;
59
+ --space-10: 2.5rem;
60
+ --space-12: 3rem;
61
+
62
+ /* Radius */
63
+ --radius-sm: 6px;
64
+ --radius-md: 8px;
65
+ --radius-lg: 12px;
66
+ --radius-xl: 16px;
67
+ --radius-full: 9999px;
68
+
69
+ /* Shadows */
70
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
71
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
72
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
73
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
74
+
75
+ /* Transitions */
76
+ --transition-fast: 150ms ease;
77
+ --transition-base: 200ms ease;
78
+ --transition-slow: 300ms ease;
79
+
80
+ /* Layout */
81
+ --sidebar-width: 280px;
82
+ --navbar-height: 64px;
83
+ }
84
+
85
+ /* ── Theme Transition ──
86
+ Applied briefly via JS to create a single smooth crossfade.
87
+ Without this class, theme swaps are instant (no jank). */
88
+
89
+ .theme-transitioning,
90
+ .theme-transitioning *,
91
+ .theme-transitioning *::before,
92
+ .theme-transitioning *::after {
93
+ transition: background-color 400ms ease, color 400ms ease,
94
+ border-color 400ms ease, box-shadow 400ms ease,
95
+ fill 400ms ease, stroke 400ms ease !important;
96
+ }
97
+
98
+ /* ── Light Theme (default) ── */
99
+
100
+ [data-theme="light"] {
101
+ color-scheme: light;
102
+ --bg-primary: #ffffff;
103
+ --bg-secondary: var(--neutral-50);
104
+ --bg-tertiary: var(--neutral-100);
105
+ --bg-elevated: #ffffff;
106
+ --bg-overlay: rgba(0, 0, 0, 0.5);
107
+
108
+ --text-primary: var(--neutral-900);
109
+ --text-secondary: var(--neutral-600);
110
+ --text-tertiary: var(--neutral-400);
111
+ --text-inverse: #ffffff;
112
+
113
+ --border-primary: var(--neutral-200);
114
+ --border-secondary: var(--neutral-100);
115
+ --border-focus: var(--accent-500);
116
+
117
+ --surface-card: #ffffff;
118
+ --surface-card-hover: var(--neutral-50);
119
+ --surface-input: #ffffff;
120
+
121
+ --glass-bg: rgba(255, 255, 255, 0.8);
122
+ --glass-border: rgba(255, 255, 255, 0.3);
123
+
124
+ --option-bg: var(--neutral-100);
125
+ --option-hover: var(--neutral-200);
126
+ --option-selected-bg: var(--accent-50);
127
+ --option-selected-border: var(--accent-500);
128
+ --option-selected-text: var(--accent-700);
129
+
130
+ --checkbox-bg: var(--neutral-100);
131
+ --checkbox-checked-bg: var(--accent-500);
132
+
133
+ --scrollbar-track: var(--neutral-100);
134
+ --scrollbar-thumb: var(--neutral-300);
135
+
136
+ --sidebar-bg: var(--neutral-50);
137
+ --navbar-bg: #ffffff;
138
+ --navbar-border: var(--neutral-200);
139
+ }
140
+
141
+ /* ── Dark Theme ── */
142
+
143
+ [data-theme="dark"] {
144
+ color-scheme: dark;
145
+ --bg-primary: #0a0f1a;
146
+ --bg-secondary: #0f1629;
147
+ --bg-tertiary: #151d35;
148
+ --bg-elevated: #1a2340;
149
+ --bg-overlay: rgba(0, 0, 0, 0.7);
150
+
151
+ --text-primary: #e8ecf4;
152
+ --text-secondary: #94a3b8;
153
+ --text-tertiary: #64748b;
154
+ --text-inverse: #0a0f1a;
155
+
156
+ --border-primary: #1e2d4a;
157
+ --border-secondary: #162036;
158
+ --border-focus: var(--accent-400);
159
+
160
+ --surface-card: #111a30;
161
+ --surface-card-hover: #162040;
162
+ --surface-input: #0f1629;
163
+
164
+ --glass-bg: rgba(17, 26, 48, 0.85);
165
+ --glass-border: rgba(30, 45, 74, 0.5);
166
+
167
+ --option-bg: #151d35;
168
+ --option-hover: #1a2545;
169
+ --option-selected-bg: rgba(16, 185, 129, 0.12);
170
+ --option-selected-border: var(--accent-500);
171
+ --option-selected-text: var(--accent-400);
172
+
173
+ --checkbox-bg: #1a2340;
174
+ --checkbox-checked-bg: var(--accent-500);
175
+
176
+ --scrollbar-track: #0f1629;
177
+ --scrollbar-thumb: #1e2d4a;
178
+
179
+ --sidebar-bg: #0c1220;
180
+ --navbar-bg: #0c1220;
181
+ --navbar-border: #1e2d4a;
182
+ }
183
+
184
+ /* ── Global Reset ── */
185
+
186
+ *,
187
+ *::before,
188
+ *::after {
189
+ margin: 0;
190
+ padding: 0;
191
+ box-sizing: border-box;
192
+ }
193
+
194
+ html {
195
+ font-size: 16px;
196
+ scroll-behavior: smooth;
197
+ -webkit-font-smoothing: antialiased;
198
+ -moz-osx-font-smoothing: grayscale;
199
+ }
200
+
201
+ body {
202
+ font-family: var(--font-family);
203
+ background: var(--bg-primary);
204
+ color: var(--text-primary);
205
+ line-height: 1.6;
206
+ min-height: 100vh;
207
+ }
208
+
209
+ #root {
210
+ min-height: 100vh;
211
+ }
212
+
213
+ /* ── Instant theme swap β€” disables ALL transitions during toggle ── */
214
+ .theme-switching,
215
+ .theme-switching *,
216
+ .theme-switching *::before,
217
+ .theme-switching *::after {
218
+ transition: none !important;
219
+ animation-duration: 0s !important;
220
+ }
221
+
222
+ /* ── Scrollbar ── */
223
+
224
+ ::-webkit-scrollbar {
225
+ width: 6px;
226
+ height: 6px;
227
+ }
228
+
229
+ ::-webkit-scrollbar-track {
230
+ background: var(--scrollbar-track);
231
+ }
232
+
233
+ ::-webkit-scrollbar-thumb {
234
+ background: var(--scrollbar-thumb);
235
+ border-radius: var(--radius-full);
236
+ }
237
+
238
+ ::-webkit-scrollbar-thumb:hover {
239
+ background: var(--accent-500);
240
+ }
241
+
242
+ /* ═══════════════════════════════════════════════════════════
243
+ Layout
244
+ ═══════════════════════════════════════════════════════════ */
245
+
246
+ .app-layout {
247
+ display: flex;
248
+ min-height: 100vh;
249
+ }
250
+
251
+ /* ── Navbar ── */
252
+
253
+ .navbar {
254
+ position: fixed;
255
+ top: 0;
256
+ left: 0;
257
+ right: 0;
258
+ height: var(--navbar-height);
259
+ background: var(--navbar-bg);
260
+ border-bottom: 1px solid var(--navbar-border);
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: space-between;
264
+ padding: 0 var(--space-6);
265
+ z-index: 100;
266
+ backdrop-filter: blur(20px);
267
+ -webkit-backdrop-filter: blur(20px);
268
+ }
269
+
270
+ /* Hamburger β€” hidden on desktop */
271
+ .navbar__hamburger {
272
+ display: none;
273
+ align-items: center;
274
+ justify-content: center;
275
+ width: 36px;
276
+ height: 36px;
277
+ border: 1px solid var(--border-primary);
278
+ border-radius: var(--radius-md);
279
+ background: var(--surface-card);
280
+ color: var(--text-secondary);
281
+ cursor: pointer;
282
+ transition: all var(--transition-fast);
283
+ flex-shrink: 0;
284
+ }
285
+
286
+ .navbar__hamburger:hover {
287
+ border-color: var(--accent-500);
288
+ color: var(--accent-500);
289
+ }
290
+
291
+ /* Sidebar backdrop overlay β€” mobile only */
292
+ .sidebar-backdrop {
293
+ display: none;
294
+ }
295
+
296
+ .navbar__brand {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: var(--space-3);
300
+ }
301
+
302
+ .navbar__icon {
303
+ width: 36px;
304
+ height: 36px;
305
+ background: linear-gradient(135deg, var(--accent-500), var(--accent-700));
306
+ border-radius: var(--radius-md);
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ color: white;
311
+ }
312
+
313
+ .navbar__title {
314
+ font-size: var(--font-lg);
315
+ font-weight: 700;
316
+ letter-spacing: -0.025em;
317
+ }
318
+
319
+ .navbar__title-accent {
320
+ color: var(--accent-500);
321
+ }
322
+
323
+ .navbar__actions {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: var(--space-3);
327
+ }
328
+
329
+ .navbar__btn {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: var(--space-2);
333
+ padding: var(--space-2) var(--space-4);
334
+ border: 1px solid var(--border-primary);
335
+ border-radius: var(--radius-md);
336
+ background: var(--surface-card);
337
+ color: var(--text-primary);
338
+ font-size: var(--font-sm);
339
+ font-weight: 500;
340
+ cursor: pointer;
341
+ transition: all var(--transition-fast);
342
+ font-family: var(--font-family);
343
+ }
344
+
345
+ .navbar__btn:hover {
346
+ border-color: var(--accent-500);
347
+ color: var(--accent-500);
348
+ }
349
+
350
+ .navbar__btn--primary {
351
+ background: linear-gradient(135deg, var(--accent-500), var(--accent-600));
352
+ color: white;
353
+ border-color: transparent;
354
+ }
355
+
356
+ .navbar__btn--primary:hover {
357
+ background: linear-gradient(135deg, var(--accent-600), var(--accent-700));
358
+ color: white;
359
+ transform: translateY(-1px);
360
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
361
+ }
362
+
363
+ .theme-toggle {
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ width: 36px;
368
+ height: 36px;
369
+ border: 1px solid var(--border-primary);
370
+ border-radius: var(--radius-md);
371
+ background: var(--surface-card);
372
+ color: var(--text-secondary);
373
+ cursor: pointer;
374
+ transition: all var(--transition-fast);
375
+ }
376
+
377
+ .theme-toggle:hover {
378
+ border-color: var(--accent-500);
379
+ color: var(--accent-500);
380
+ }
381
+
382
+ /* ── Sidebar ── */
383
+
384
+ .sidebar {
385
+ position: fixed;
386
+ top: var(--navbar-height);
387
+ left: 0;
388
+ width: var(--sidebar-width);
389
+ height: calc(100vh - var(--navbar-height));
390
+ background: var(--sidebar-bg);
391
+ border-right: 1px solid var(--border-primary);
392
+ overflow-y: auto;
393
+ padding: var(--space-4) 0;
394
+ z-index: 50;
395
+ }
396
+
397
+ .sidebar__section-title {
398
+ font-size: var(--font-xs);
399
+ font-weight: 600;
400
+ text-transform: uppercase;
401
+ letter-spacing: 0.08em;
402
+ color: var(--text-tertiary);
403
+ padding: var(--space-3) var(--space-5);
404
+ margin-top: var(--space-2);
405
+ }
406
+
407
+ .sidebar__item {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: var(--space-3);
411
+ padding: var(--space-3) var(--space-5);
412
+ cursor: pointer;
413
+ transition: all var(--transition-fast);
414
+ border-left: 3px solid transparent;
415
+ color: var(--text-secondary);
416
+ font-size: var(--font-sm);
417
+ font-weight: 500;
418
+ position: relative;
419
+ }
420
+
421
+ .sidebar__item:hover {
422
+ background: var(--surface-card-hover);
423
+ color: var(--text-primary);
424
+ }
425
+
426
+ .sidebar__item--active {
427
+ background: var(--option-selected-bg);
428
+ border-left-color: var(--accent-500);
429
+ color: var(--accent-500);
430
+ }
431
+
432
+ .sidebar__item-icon {
433
+ width: 20px;
434
+ height: 20px;
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ flex-shrink: 0;
439
+ }
440
+
441
+ .sidebar__item-label {
442
+ white-space: nowrap;
443
+ overflow: hidden;
444
+ text-overflow: ellipsis;
445
+ }
446
+
447
+ .sidebar__item-badge {
448
+ margin-left: auto;
449
+ background: var(--accent-500);
450
+ color: white;
451
+ font-size: 10px;
452
+ font-weight: 700;
453
+ padding: 2px 6px;
454
+ border-radius: var(--radius-full);
455
+ min-width: 20px;
456
+ text-align: center;
457
+ }
458
+
459
+ .sidebar__toggle-section {
460
+ display: flex;
461
+ align-items: center;
462
+ justify-content: space-between;
463
+ padding: var(--space-2) var(--space-5);
464
+ }
465
+
466
+ /* ── Main Content ── */
467
+
468
+ .main-content {
469
+ margin-left: var(--sidebar-width);
470
+ margin-top: var(--navbar-height);
471
+ flex: 1;
472
+ padding: var(--space-8);
473
+ min-height: calc(100vh - var(--navbar-height));
474
+ max-width: 960px;
475
+ }
476
+
477
+ /* ═══════════════════════════════════════════════════════════
478
+ Section & Subsection Cards
479
+ ═══════════════════════════════════════════════════════════ */
480
+
481
+ .section {
482
+ margin-bottom: var(--space-8);
483
+ animation: fadeInUp 0.4s ease;
484
+ }
485
+
486
+ @keyframes fadeInUp {
487
+ from { opacity: 0; transform: translateY(12px); }
488
+ to { opacity: 1; transform: translateY(0); }
489
+ }
490
+
491
+ .section__header {
492
+ display: flex;
493
+ align-items: center;
494
+ gap: var(--space-3);
495
+ margin-bottom: var(--space-6);
496
+ padding-bottom: var(--space-4);
497
+ border-bottom: 2px solid var(--accent-500);
498
+ }
499
+
500
+ .section__header-icon {
501
+ width: 40px;
502
+ height: 40px;
503
+ background: linear-gradient(135deg, var(--accent-500), var(--accent-700));
504
+ border-radius: var(--radius-md);
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: center;
508
+ color: white;
509
+ flex-shrink: 0;
510
+ }
511
+
512
+ .section__title {
513
+ font-size: var(--font-xl);
514
+ font-weight: 700;
515
+ letter-spacing: -0.025em;
516
+ }
517
+
518
+ .section__cpt {
519
+ font-size: var(--font-sm);
520
+ color: var(--accent-500);
521
+ font-weight: 600;
522
+ background: var(--option-selected-bg);
523
+ padding: 2px 10px;
524
+ border-radius: var(--radius-full);
525
+ margin-left: auto;
526
+ }
527
+
528
+ .section__toggle {
529
+ margin-left: auto;
530
+ display: flex;
531
+ align-items: center;
532
+ gap: var(--space-2);
533
+ }
534
+
535
+ .toggle-switch {
536
+ position: relative;
537
+ width: 44px;
538
+ height: 24px;
539
+ background: var(--neutral-300);
540
+ border-radius: var(--radius-full);
541
+ cursor: pointer;
542
+ transition: background var(--transition-fast);
543
+ }
544
+
545
+ [data-theme="dark"] .toggle-switch {
546
+ background: var(--neutral-700);
547
+ }
548
+
549
+ .toggle-switch--active {
550
+ background: var(--accent-500) !important;
551
+ }
552
+
553
+ .toggle-switch::after {
554
+ content: '';
555
+ position: absolute;
556
+ top: 2px;
557
+ left: 2px;
558
+ width: 20px;
559
+ height: 20px;
560
+ background: white;
561
+ border-radius: 50%;
562
+ transition: transform var(--transition-fast);
563
+ box-shadow: var(--shadow-sm);
564
+ }
565
+
566
+ .toggle-switch--active::after {
567
+ transform: translateX(20px);
568
+ }
569
+
570
+ .subsection {
571
+ background: var(--surface-card);
572
+ border: 1px solid var(--border-primary);
573
+ border-radius: var(--radius-lg);
574
+ padding: var(--space-5);
575
+ margin-bottom: var(--space-4);
576
+ }
577
+
578
+ .subsection:hover {
579
+ border-color: var(--accent-500);
580
+ border-color: color-mix(in srgb, var(--accent-500) 40%, var(--border-primary));
581
+ }
582
+
583
+ .subsection__title {
584
+ font-size: var(--font-base);
585
+ font-weight: 600;
586
+ color: var(--text-primary);
587
+ margin-bottom: var(--space-4);
588
+ display: flex;
589
+ align-items: center;
590
+ gap: var(--space-2);
591
+ }
592
+
593
+ .subsection__title-dot {
594
+ width: 6px;
595
+ height: 6px;
596
+ background: var(--accent-500);
597
+ border-radius: 50%;
598
+ flex-shrink: 0;
599
+ }
600
+
601
+ /* ═══════════════════════════════════════════════════════════
602
+ Field Components
603
+ ═══════════════════════════════════════════════════════════ */
604
+
605
+ /* ── Fill Blank ── */
606
+
607
+ .fill-blank {
608
+ margin-bottom: var(--space-5);
609
+ }
610
+
611
+ .fill-blank__label {
612
+ font-size: var(--font-sm);
613
+ font-weight: 600;
614
+ color: var(--text-secondary);
615
+ margin-bottom: var(--space-2);
616
+ text-transform: uppercase;
617
+ letter-spacing: 0.04em;
618
+ }
619
+
620
+ .fill-blank__sentence {
621
+ font-size: var(--font-base);
622
+ color: var(--text-primary);
623
+ margin-bottom: var(--space-3);
624
+ line-height: 1.7;
625
+ padding: var(--space-3) var(--space-4);
626
+ background: var(--bg-secondary);
627
+ border-radius: var(--radius-md);
628
+ border-left: 3px solid var(--accent-500);
629
+ }
630
+
631
+ .fill-blank__blank {
632
+ color: var(--accent-500);
633
+ font-weight: 600;
634
+ border-bottom: 2px dashed var(--accent-400);
635
+ padding: 0 var(--space-1);
636
+ }
637
+
638
+ .fill-blank__blank--filled {
639
+ color: var(--accent-400);
640
+ border-bottom-style: solid;
641
+ background: var(--option-selected-bg);
642
+ border-radius: 2px;
643
+ padding: 1px var(--space-1);
644
+ }
645
+
646
+ .fill-blank__options {
647
+ display: flex;
648
+ flex-wrap: wrap;
649
+ gap: var(--space-2);
650
+ }
651
+
652
+ .option-btn {
653
+ padding: var(--space-2) var(--space-3);
654
+ border: 1px solid var(--border-primary);
655
+ border-radius: var(--radius-full);
656
+ background: var(--option-bg);
657
+ color: var(--text-secondary);
658
+ font-size: var(--font-sm);
659
+ font-weight: 500;
660
+ cursor: pointer;
661
+ transition: all var(--transition-fast);
662
+ font-family: var(--font-family);
663
+ white-space: nowrap;
664
+ }
665
+
666
+ .option-btn:hover {
667
+ background: var(--option-hover);
668
+ color: var(--text-primary);
669
+ border-color: var(--accent-400);
670
+ }
671
+
672
+ .option-btn--selected {
673
+ background: var(--option-selected-bg);
674
+ border-color: var(--option-selected-border);
675
+ color: var(--option-selected-text);
676
+ font-weight: 600;
677
+ }
678
+
679
+ .option-btn--selected:hover {
680
+ background: var(--option-selected-bg);
681
+ border-color: var(--accent-400);
682
+ }
683
+
684
+ /* ── Custom input inline ── */
685
+
686
+ .fill-blank__custom {
687
+ display: flex;
688
+ align-items: center;
689
+ gap: var(--space-2);
690
+ margin-top: var(--space-2);
691
+ }
692
+
693
+ .fill-blank__custom-input {
694
+ flex: 1;
695
+ padding: var(--space-2) var(--space-3);
696
+ border: 1px solid var(--border-primary);
697
+ border-radius: var(--radius-full);
698
+ background: var(--surface-input);
699
+ color: var(--text-primary);
700
+ font-size: var(--font-sm);
701
+ font-family: var(--font-family);
702
+ transition: border-color var(--transition-fast);
703
+ max-width: 300px;
704
+ }
705
+
706
+ .fill-blank__custom-input:focus {
707
+ outline: none;
708
+ border-color: var(--accent-500);
709
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
710
+ }
711
+
712
+ .fill-blank__custom-add {
713
+ width: 32px;
714
+ height: 32px;
715
+ border: 1px solid var(--accent-500);
716
+ border-radius: 50%;
717
+ background: transparent;
718
+ color: var(--accent-500);
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: center;
722
+ cursor: pointer;
723
+ transition: all var(--transition-fast);
724
+ flex-shrink: 0;
725
+ }
726
+
727
+ .fill-blank__custom-add:hover {
728
+ background: var(--accent-500);
729
+ color: white;
730
+ }
731
+
732
+ /* ── Checkbox Group ── */
733
+
734
+ .checkbox-group {
735
+ margin-bottom: var(--space-5);
736
+ }
737
+
738
+ .checkbox-group__label {
739
+ font-size: var(--font-sm);
740
+ font-weight: 600;
741
+ color: var(--text-secondary);
742
+ margin-bottom: var(--space-2);
743
+ text-transform: uppercase;
744
+ letter-spacing: 0.04em;
745
+ }
746
+
747
+ .checkbox-group__sentence {
748
+ font-size: var(--font-base);
749
+ color: var(--text-primary);
750
+ margin-bottom: var(--space-3);
751
+ line-height: 1.7;
752
+ padding: var(--space-3) var(--space-4);
753
+ background: var(--bg-secondary);
754
+ border-radius: var(--radius-md);
755
+ border-left: 3px solid var(--accent-500);
756
+ }
757
+
758
+ .checkbox-group__items {
759
+ display: flex;
760
+ flex-direction: column;
761
+ gap: var(--space-2);
762
+ }
763
+
764
+ .checkbox-item {
765
+ display: flex;
766
+ align-items: center;
767
+ gap: var(--space-3);
768
+ padding: var(--space-3) var(--space-4);
769
+ border: 1px solid var(--border-primary);
770
+ border-radius: var(--radius-md);
771
+ background: var(--surface-card);
772
+ cursor: pointer;
773
+ transition: all var(--transition-fast);
774
+ }
775
+
776
+ .checkbox-item:hover {
777
+ border-color: var(--accent-400);
778
+ background: var(--surface-card-hover);
779
+ }
780
+
781
+ .checkbox-item--checked {
782
+ background: var(--option-selected-bg);
783
+ border-color: var(--option-selected-border);
784
+ }
785
+
786
+ .checkbox-item__box {
787
+ width: 20px;
788
+ height: 20px;
789
+ border: 2px solid var(--border-primary);
790
+ border-radius: var(--radius-sm);
791
+ display: flex;
792
+ align-items: center;
793
+ justify-content: center;
794
+ flex-shrink: 0;
795
+ transition: all var(--transition-fast);
796
+ }
797
+
798
+ .checkbox-item--checked .checkbox-item__box {
799
+ background: var(--accent-500);
800
+ border-color: var(--accent-500);
801
+ color: white;
802
+ }
803
+
804
+ .checkbox-item__label {
805
+ font-size: var(--font-sm);
806
+ color: var(--text-primary);
807
+ font-weight: 500;
808
+ }
809
+
810
+ /* ── Exercise List ── */
811
+
812
+ .exercise-list {
813
+ margin-bottom: var(--space-5);
814
+ }
815
+
816
+ .exercise-list__label {
817
+ font-size: var(--font-sm);
818
+ font-weight: 600;
819
+ color: var(--text-secondary);
820
+ margin-bottom: var(--space-3);
821
+ text-transform: uppercase;
822
+ letter-spacing: 0.04em;
823
+ }
824
+
825
+ .exercise-item {
826
+ display: flex;
827
+ align-items: center;
828
+ gap: var(--space-3);
829
+ padding: var(--space-2) var(--space-3);
830
+ border: 1px solid var(--border-secondary);
831
+ border-radius: var(--radius-md);
832
+ margin-bottom: var(--space-2);
833
+ transition: all var(--transition-fast);
834
+ background: var(--surface-card);
835
+ }
836
+
837
+ .exercise-item--active {
838
+ border-color: var(--accent-500);
839
+ background: var(--option-selected-bg);
840
+ }
841
+
842
+ .exercise-item__toggle {
843
+ width: 20px;
844
+ height: 20px;
845
+ border: 2px solid var(--border-primary);
846
+ border-radius: var(--radius-sm);
847
+ display: flex;
848
+ align-items: center;
849
+ justify-content: center;
850
+ cursor: pointer;
851
+ flex-shrink: 0;
852
+ transition: all var(--transition-fast);
853
+ }
854
+
855
+ .exercise-item--active .exercise-item__toggle {
856
+ background: var(--accent-500);
857
+ border-color: var(--accent-500);
858
+ color: white;
859
+ }
860
+
861
+ .exercise-item__name {
862
+ flex: 1;
863
+ font-size: var(--font-sm);
864
+ color: var(--text-primary);
865
+ font-weight: 500;
866
+ cursor: pointer;
867
+ }
868
+
869
+ .exercise-item__inputs {
870
+ display: flex;
871
+ align-items: center;
872
+ gap: var(--space-2);
873
+ }
874
+
875
+ .exercise-item__input {
876
+ width: 48px;
877
+ padding: var(--space-1) var(--space-2);
878
+ border: 1px solid var(--border-primary);
879
+ border-radius: var(--radius-sm);
880
+ background: var(--surface-input);
881
+ color: var(--text-primary);
882
+ font-size: var(--font-xs);
883
+ text-align: center;
884
+ font-family: var(--font-family);
885
+ transition: border-color var(--transition-fast);
886
+ }
887
+
888
+ .exercise-item__input:focus {
889
+ outline: none;
890
+ border-color: var(--accent-500);
891
+ }
892
+
893
+ .exercise-item__separator {
894
+ color: var(--text-tertiary);
895
+ font-size: var(--font-xs);
896
+ font-weight: 600;
897
+ }
898
+
899
+ .exercise-item__unit {
900
+ color: var(--text-tertiary);
901
+ font-size: var(--font-xs);
902
+ min-width: 32px;
903
+ }
904
+
905
+ /* ── Text / Number Input ── */
906
+
907
+ .text-input {
908
+ margin-bottom: var(--space-4);
909
+ }
910
+
911
+ .text-input__label {
912
+ display: block;
913
+ font-size: var(--font-sm);
914
+ font-weight: 600;
915
+ color: var(--text-secondary);
916
+ margin-bottom: var(--space-2);
917
+ }
918
+
919
+ .text-input__field {
920
+ width: 100%;
921
+ padding: var(--space-3) var(--space-4);
922
+ border: 1px solid var(--border-primary);
923
+ border-radius: var(--radius-md);
924
+ background: var(--surface-input);
925
+ color: var(--text-primary);
926
+ font-size: var(--font-base);
927
+ font-family: var(--font-family);
928
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
929
+ }
930
+
931
+ .text-input__field:focus {
932
+ outline: none;
933
+ border-color: var(--accent-500);
934
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
935
+ }
936
+
937
+ .text-input__field::placeholder {
938
+ color: var(--text-tertiary);
939
+ }
940
+
941
+ .text-input__sentence {
942
+ font-size: var(--font-sm);
943
+ color: var(--text-tertiary);
944
+ margin-top: var(--space-1);
945
+ font-style: italic;
946
+ }
947
+
948
+ /* ── Static Text ── */
949
+
950
+ .static-text {
951
+ padding: var(--space-4);
952
+ background: var(--bg-secondary);
953
+ border-radius: var(--radius-md);
954
+ border-left: 3px solid var(--accent-500);
955
+ margin-bottom: var(--space-4);
956
+ }
957
+
958
+ .static-text__content {
959
+ font-size: var(--font-sm);
960
+ color: var(--text-secondary);
961
+ line-height: 1.7;
962
+ }
963
+
964
+ /* ═══════════════════════════════════════════════════════════
965
+ Header / Patient Info
966
+ ═══════════════════════════════════════════════════════════ */
967
+
968
+ .patient-header {
969
+ display: grid;
970
+ grid-template-columns: 1fr 1fr;
971
+ gap: var(--space-4);
972
+ margin-bottom: var(--space-6);
973
+ }
974
+
975
+ .patient-header .text-input__field {
976
+ background: var(--surface-card);
977
+ }
978
+
979
+ /* ═══════════════════════════════════════════════════════════
980
+ Abbreviation Reference
981
+ ═══════════════════════════════════════════════════════════ */
982
+
983
+ .abbrev-panel {
984
+ margin-bottom: var(--space-8);
985
+ }
986
+
987
+ .abbrev-category {
988
+ margin-bottom: var(--space-4);
989
+ }
990
+
991
+ .abbrev-category__title {
992
+ font-size: var(--font-xs);
993
+ font-weight: 700;
994
+ text-transform: uppercase;
995
+ letter-spacing: 0.08em;
996
+ color: var(--accent-500);
997
+ margin-bottom: var(--space-2);
998
+ padding-bottom: var(--space-1);
999
+ border-bottom: 1px solid var(--border-secondary);
1000
+ }
1001
+
1002
+ .abbrev-list {
1003
+ display: flex;
1004
+ flex-wrap: wrap;
1005
+ gap: var(--space-2);
1006
+ }
1007
+
1008
+ .abbrev-chip {
1009
+ display: inline-flex;
1010
+ align-items: center;
1011
+ gap: 4px;
1012
+ padding: 3px 10px;
1013
+ border: 1px solid var(--border-primary);
1014
+ border-radius: var(--radius-full);
1015
+ font-size: var(--font-xs);
1016
+ background: var(--surface-card);
1017
+ }
1018
+
1019
+ .abbrev-chip__abbr {
1020
+ font-weight: 700;
1021
+ color: var(--accent-500);
1022
+ }
1023
+
1024
+ .abbrev-chip__meaning {
1025
+ color: var(--text-secondary);
1026
+ }
1027
+
1028
+ /* ═══════════════════════════════════════════════════════════
1029
+ Preview / Generated Note Panel
1030
+ ═══════════════════════════════════════════════════════════ */
1031
+
1032
+ .preview-overlay {
1033
+ position: fixed;
1034
+ inset: 0;
1035
+ background: var(--bg-overlay);
1036
+ z-index: 200;
1037
+ display: flex;
1038
+ align-items: center;
1039
+ justify-content: center;
1040
+ animation: fadeIn 0.2s ease;
1041
+ }
1042
+
1043
+ @keyframes fadeIn {
1044
+ from { opacity: 0; }
1045
+ to { opacity: 1; }
1046
+ }
1047
+
1048
+ .preview-panel {
1049
+ width: 90%;
1050
+ max-width: 800px;
1051
+ max-height: 90vh;
1052
+ background: var(--bg-elevated);
1053
+ border: 1px solid var(--border-primary);
1054
+ border-radius: var(--radius-xl);
1055
+ overflow: hidden;
1056
+ display: flex;
1057
+ flex-direction: column;
1058
+ box-shadow: var(--shadow-xl);
1059
+ animation: slideUp 0.3s ease;
1060
+ }
1061
+
1062
+ @keyframes slideUp {
1063
+ from { opacity: 0; transform: translateY(20px); }
1064
+ to { opacity: 1; transform: translateY(0); }
1065
+ }
1066
+
1067
+ .preview-panel__header {
1068
+ display: flex;
1069
+ align-items: center;
1070
+ justify-content: space-between;
1071
+ padding: var(--space-5) var(--space-6);
1072
+ border-bottom: 1px solid var(--border-primary);
1073
+ }
1074
+
1075
+ .preview-panel__title {
1076
+ font-size: var(--font-lg);
1077
+ font-weight: 700;
1078
+ }
1079
+
1080
+ .preview-panel__close {
1081
+ width: 32px;
1082
+ height: 32px;
1083
+ border: 1px solid var(--border-primary);
1084
+ border-radius: var(--radius-md);
1085
+ background: transparent;
1086
+ color: var(--text-secondary);
1087
+ display: flex;
1088
+ align-items: center;
1089
+ justify-content: center;
1090
+ cursor: pointer;
1091
+ transition: all var(--transition-fast);
1092
+ }
1093
+
1094
+ .preview-panel__close:hover {
1095
+ border-color: var(--danger);
1096
+ color: var(--danger);
1097
+ }
1098
+
1099
+ .preview-panel__body {
1100
+ flex: 1;
1101
+ overflow-y: auto;
1102
+ padding: var(--space-6);
1103
+ }
1104
+
1105
+ .preview-panel__content {
1106
+ font-size: var(--font-base);
1107
+ line-height: 1.8;
1108
+ white-space: pre-wrap;
1109
+ color: var(--text-primary);
1110
+ }
1111
+
1112
+ .preview-panel__content p {
1113
+ margin-bottom: var(--space-4);
1114
+ }
1115
+
1116
+ .preview-panel__footer {
1117
+ display: flex;
1118
+ align-items: center;
1119
+ gap: var(--space-3);
1120
+ padding: var(--space-4) var(--space-6);
1121
+ border-top: 1px solid var(--border-primary);
1122
+ }
1123
+
1124
+ .preview-panel__footer .navbar__btn {
1125
+ flex: 1;
1126
+ justify-content: center;
1127
+ }
1128
+
1129
+ /* ── Loading ── */
1130
+
1131
+ .loading-container {
1132
+ display: flex;
1133
+ flex-direction: column;
1134
+ align-items: center;
1135
+ justify-content: center;
1136
+ padding: var(--space-12);
1137
+ gap: var(--space-4);
1138
+ }
1139
+
1140
+ .loading-spinner {
1141
+ width: 40px;
1142
+ height: 40px;
1143
+ border: 3px solid var(--border-primary);
1144
+ border-top-color: var(--accent-500);
1145
+ border-radius: 50%;
1146
+ animation: spin 0.8s linear infinite;
1147
+ }
1148
+
1149
+ @keyframes spin {
1150
+ to { transform: rotate(360deg); }
1151
+ }
1152
+
1153
+ .loading-text {
1154
+ font-size: var(--font-sm);
1155
+ color: var(--text-secondary);
1156
+ font-weight: 500;
1157
+ }
1158
+
1159
+ /* ═══════════════════════════════════════════════════════════
1160
+ Smart Phrases Panel
1161
+ ═══════════════════════════════════════════════════════════ */
1162
+
1163
+ .smart-phrases__list {
1164
+ display: flex;
1165
+ flex-direction: column;
1166
+ gap: var(--space-3);
1167
+ }
1168
+
1169
+ .smart-phrase {
1170
+ padding: var(--space-3) var(--space-4);
1171
+ background: var(--surface-card);
1172
+ border: 1px solid var(--border-primary);
1173
+ border-radius: var(--radius-md);
1174
+ font-size: var(--font-sm);
1175
+ color: var(--text-secondary);
1176
+ line-height: 1.6;
1177
+ display: flex;
1178
+ gap: var(--space-3);
1179
+ transition: all var(--transition-fast);
1180
+ cursor: pointer;
1181
+ }
1182
+
1183
+ .smart-phrase:hover {
1184
+ border-color: var(--accent-500);
1185
+ background: var(--option-selected-bg);
1186
+ }
1187
+
1188
+ .smart-phrase__number {
1189
+ color: var(--accent-500);
1190
+ font-weight: 700;
1191
+ flex-shrink: 0;
1192
+ min-width: 24px;
1193
+ }
1194
+
1195
+ .smart-phrase__copy {
1196
+ margin-left: auto;
1197
+ color: var(--text-tertiary);
1198
+ flex-shrink: 0;
1199
+ transition: color var(--transition-fast);
1200
+ }
1201
+
1202
+ .smart-phrase:hover .smart-phrase__copy {
1203
+ color: var(--accent-500);
1204
+ }
1205
+
1206
+ /* ═══════════════════════════════════════════════════════════
1207
+ Custom Section Modal
1208
+ ═══════════════════════════════════════════════════════════ */
1209
+
1210
+ .modal-overlay {
1211
+ position: fixed;
1212
+ inset: 0;
1213
+ background: var(--bg-overlay);
1214
+ z-index: 300;
1215
+ display: flex;
1216
+ align-items: center;
1217
+ justify-content: center;
1218
+ }
1219
+
1220
+ .modal {
1221
+ width: 90%;
1222
+ max-width: 500px;
1223
+ background: var(--bg-elevated);
1224
+ border: 1px solid var(--border-primary);
1225
+ border-radius: var(--radius-xl);
1226
+ padding: var(--space-6);
1227
+ box-shadow: var(--shadow-xl);
1228
+ }
1229
+
1230
+ .modal__title {
1231
+ font-size: var(--font-lg);
1232
+ font-weight: 700;
1233
+ margin-bottom: var(--space-5);
1234
+ }
1235
+
1236
+ .modal__actions {
1237
+ display: flex;
1238
+ justify-content: flex-end;
1239
+ gap: var(--space-3);
1240
+ margin-top: var(--space-5);
1241
+ }
1242
+
1243
+ /* ═══════════════════════════════════════════════════════════
1244
+ Responsive β€” Tablet (max 1024px)
1245
+ ══════════��════════════════════════════════════════════════ */
1246
+
1247
+ @media (max-width: 1024px) {
1248
+ /* Show hamburger menu button */
1249
+ .navbar__hamburger {
1250
+ display: flex;
1251
+ }
1252
+
1253
+ /* Sidebar backdrop */
1254
+ .sidebar-backdrop {
1255
+ display: block;
1256
+ position: fixed;
1257
+ inset: 0;
1258
+ background: rgba(0, 0, 0, 0.4);
1259
+ z-index: 85;
1260
+ backdrop-filter: blur(2px);
1261
+ }
1262
+
1263
+ /* Sidebar -> overlay drawer */
1264
+ .sidebar {
1265
+ transform: translateX(-100%);
1266
+ transition: transform var(--transition-slow);
1267
+ z-index: 90;
1268
+ box-shadow: none;
1269
+ }
1270
+
1271
+ .sidebar--open {
1272
+ transform: translateX(0);
1273
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.2);
1274
+ }
1275
+
1276
+ /* Main content takes full width */
1277
+ .main-content {
1278
+ margin-left: 0;
1279
+ padding: var(--space-6);
1280
+ max-width: 100%;
1281
+ }
1282
+
1283
+ /* Navbar button labels can stay */
1284
+ .navbar {
1285
+ padding: 0 var(--space-4);
1286
+ }
1287
+
1288
+ .navbar__actions {
1289
+ gap: var(--space-2);
1290
+ }
1291
+
1292
+ /* Panels fill more screen */
1293
+ .preview-panel {
1294
+ width: 95%;
1295
+ max-width: 95%;
1296
+ max-height: 95vh;
1297
+ }
1298
+ }
1299
+
1300
+ /* ═══════════════════════════════════════════════════════════
1301
+ Responsive β€” Mobile (max 768px)
1302
+ ═══════════════════════════════════════════════════════════ */
1303
+
1304
+ @media (max-width: 768px) {
1305
+ /* Navbar compact */
1306
+ .navbar {
1307
+ padding: 0 var(--space-2);
1308
+ height: 52px;
1309
+ overflow-x: hidden;
1310
+ }
1311
+
1312
+ .navbar__brand {
1313
+ gap: var(--space-2);
1314
+ min-width: 0;
1315
+ }
1316
+
1317
+ /* Hide the brand icon on mobile to save space */
1318
+ .navbar__icon--desktop {
1319
+ display: none;
1320
+ }
1321
+
1322
+ .navbar__title {
1323
+ font-size: var(--font-sm);
1324
+ white-space: nowrap;
1325
+ }
1326
+
1327
+ .navbar__actions {
1328
+ gap: 4px;
1329
+ flex-shrink: 0;
1330
+ }
1331
+
1332
+ /* Hide only the label text β€” NOT all span children */
1333
+ .navbar__btn-label {
1334
+ display: none;
1335
+ }
1336
+
1337
+ .navbar__btn {
1338
+ padding: 6px;
1339
+ min-width: 32px;
1340
+ min-height: 32px;
1341
+ justify-content: center;
1342
+ }
1343
+
1344
+ /* Hide user pill on mobile */
1345
+ .navbar__user {
1346
+ display: none;
1347
+ }
1348
+
1349
+ .theme-toggle {
1350
+ width: 32px;
1351
+ height: 32px;
1352
+ flex-shrink: 0;
1353
+ }
1354
+
1355
+ .theme-toggle svg {
1356
+ width: 14px;
1357
+ height: 14px;
1358
+ }
1359
+
1360
+ .navbar__hamburger {
1361
+ width: 32px;
1362
+ height: 32px;
1363
+ }
1364
+
1365
+ /* Sidebar overlay width */
1366
+ .sidebar {
1367
+ width: 280px;
1368
+ }
1369
+
1370
+ /* Main content */
1371
+ .main-content {
1372
+ padding: var(--space-3);
1373
+ margin-top: 52px;
1374
+ }
1375
+
1376
+ /* Section header */
1377
+ .section__header {
1378
+ flex-wrap: wrap;
1379
+ gap: var(--space-2);
1380
+ }
1381
+
1382
+ .section__header-icon {
1383
+ width: 32px;
1384
+ height: 32px;
1385
+ }
1386
+
1387
+ .section__title {
1388
+ font-size: var(--font-lg);
1389
+ word-break: break-word;
1390
+ }
1391
+
1392
+ .section__toggle {
1393
+ width: 100%;
1394
+ justify-content: flex-end;
1395
+ }
1396
+
1397
+ /* Subsection cards */
1398
+ .subsection {
1399
+ padding: var(--space-3);
1400
+ }
1401
+
1402
+ /* Patient info -> single column */
1403
+ .patient-header {
1404
+ grid-template-columns: 1fr;
1405
+ gap: var(--space-3);
1406
+ }
1407
+
1408
+ /* Fill-blank sentence */
1409
+ .fill-blank__sentence {
1410
+ font-size: var(--font-sm);
1411
+ padding: var(--space-2) var(--space-3);
1412
+ }
1413
+
1414
+ /* Options buttons β€” wrap naturally, touch-friendly */
1415
+ .option-btn {
1416
+ white-space: normal;
1417
+ text-align: left;
1418
+ padding: var(--space-2) var(--space-3);
1419
+ font-size: var(--font-xs);
1420
+ min-height: 36px;
1421
+ }
1422
+
1423
+ .fill-blank__options {
1424
+ gap: var(--space-1);
1425
+ }
1426
+
1427
+ /* Custom option input */
1428
+ .fill-blank__custom {
1429
+ flex-wrap: nowrap;
1430
+ }
1431
+
1432
+ .fill-blank__custom-input {
1433
+ max-width: none;
1434
+ min-width: 0;
1435
+ }
1436
+
1437
+ /* Exercise items β€” stack on mobile */
1438
+ .exercise-item {
1439
+ flex-wrap: wrap;
1440
+ gap: var(--space-2);
1441
+ padding: var(--space-2);
1442
+ }
1443
+
1444
+ .exercise-item__name {
1445
+ flex-basis: calc(100% - 32px);
1446
+ font-size: var(--font-xs);
1447
+ }
1448
+
1449
+ .exercise-item__inputs {
1450
+ width: 100%;
1451
+ padding-left: 32px;
1452
+ gap: var(--space-1);
1453
+ }
1454
+
1455
+ .exercise-item__input {
1456
+ width: 40px;
1457
+ padding: var(--space-1);
1458
+ }
1459
+
1460
+ /* Checkbox items touch-friendly */
1461
+ .checkbox-item {
1462
+ padding: var(--space-3);
1463
+ }
1464
+
1465
+ .checkbox-item__label {
1466
+ font-size: var(--font-xs);
1467
+ }
1468
+
1469
+ /* Abbreviation chips */
1470
+ .abbrev-chip {
1471
+ font-size: 10px;
1472
+ padding: 2px 6px;
1473
+ }
1474
+
1475
+ /* Smart phrases */
1476
+ .smart-phrase {
1477
+ font-size: var(--font-xs);
1478
+ padding: var(--space-2) var(--space-3);
1479
+ }
1480
+
1481
+ /* ── Preview / AI panels β€” true full screen on mobile ── */
1482
+ .preview-overlay {
1483
+ align-items: stretch;
1484
+ }
1485
+
1486
+ .preview-panel {
1487
+ width: 100%;
1488
+ max-width: 100%;
1489
+ max-height: none;
1490
+ height: 100%;
1491
+ border-radius: 0;
1492
+ box-shadow: none;
1493
+ }
1494
+
1495
+ .preview-panel__header {
1496
+ padding: var(--space-3) var(--space-4);
1497
+ position: sticky;
1498
+ top: 0;
1499
+ background: var(--bg-elevated);
1500
+ z-index: 10;
1501
+ }
1502
+
1503
+ .preview-panel__body {
1504
+ padding: var(--space-3);
1505
+ flex: 1;
1506
+ overflow-y: auto;
1507
+ -webkit-overflow-scrolling: touch;
1508
+ }
1509
+
1510
+ .preview-panel__content {
1511
+ font-size: var(--font-sm);
1512
+ word-break: break-word;
1513
+ }
1514
+
1515
+ .preview-panel__footer {
1516
+ padding: var(--space-3);
1517
+ flex-wrap: wrap;
1518
+ gap: var(--space-2);
1519
+ position: sticky;
1520
+ bottom: 0;
1521
+ background: var(--bg-elevated);
1522
+ z-index: 10;
1523
+ }
1524
+
1525
+ .preview-panel__footer .navbar__btn {
1526
+ flex: 1 1 calc(50% - 4px);
1527
+ min-width: 0;
1528
+ justify-content: center;
1529
+ padding: var(--space-2) var(--space-3);
1530
+ }
1531
+
1532
+ /* Modal */
1533
+ .modal {
1534
+ width: 95%;
1535
+ padding: var(--space-4);
1536
+ }
1537
+
1538
+ .modal__title {
1539
+ font-size: var(--font-base);
1540
+ }
1541
+
1542
+ /* Login card */
1543
+ .login-card {
1544
+ padding: var(--space-6);
1545
+ margin: var(--space-3);
1546
+ max-width: 100%;
1547
+ }
1548
+
1549
+ .login-card__title {
1550
+ font-size: var(--font-xl);
1551
+ }
1552
+
1553
+ /* Static text */
1554
+ .static-text {
1555
+ padding: var(--space-3);
1556
+ font-size: var(--font-xs);
1557
+ }
1558
+
1559
+ .static-text__content {
1560
+ font-size: var(--font-xs);
1561
+ line-height: 1.6;
1562
+ }
1563
+
1564
+ /* Text inputs β€” 16px prevents iOS zoom */
1565
+ .text-input__field {
1566
+ font-size: 16px;
1567
+ }
1568
+
1569
+ .login-card__input {
1570
+ font-size: 16px;
1571
+ }
1572
+ }
1573
+
1574
+ /* ═══════════════════════════════════════════════════════════
1575
+ Responsive β€” Small phones (max 420px)
1576
+ ═══════════════════════════════════════════════════════════ */
1577
+
1578
+ @media (max-width: 420px) {
1579
+ .navbar__title {
1580
+ font-size: 12px;
1581
+ }
1582
+
1583
+ .navbar__hamburger {
1584
+ width: 28px;
1585
+ height: 28px;
1586
+ }
1587
+
1588
+ .navbar__btn {
1589
+ padding: 5px;
1590
+ min-width: 28px;
1591
+ min-height: 28px;
1592
+ }
1593
+
1594
+ .theme-toggle {
1595
+ width: 28px;
1596
+ height: 28px;
1597
+ }
1598
+
1599
+ .main-content {
1600
+ padding: var(--space-2);
1601
+ }
1602
+
1603
+ .section__title {
1604
+ font-size: var(--font-base);
1605
+ }
1606
+
1607
+ .subsection {
1608
+ padding: var(--space-2);
1609
+ border-radius: var(--radius-md);
1610
+ }
1611
+
1612
+ .option-btn {
1613
+ font-size: 11px;
1614
+ padding: 6px 10px;
1615
+ }
1616
+
1617
+ .fill-blank__label {
1618
+ font-size: 11px;
1619
+ }
1620
+
1621
+ .exercise-item__inputs {
1622
+ padding-left: 0;
1623
+ }
1624
+
1625
+ .login-card {
1626
+ padding: var(--space-4);
1627
+ border-radius: var(--radius-lg);
1628
+ }
1629
+
1630
+ .login-card__icon {
1631
+ width: 44px;
1632
+ height: 44px;
1633
+ }
1634
+ }
1635
+
1636
+ /* ═══════════════════════════════════════════════════════════
1637
+ Utilities
1638
+ ═══════════════════════════════════════════════════════════ */
1639
+
1640
+ .sr-only {
1641
+ position: absolute;
1642
+ width: 1px;
1643
+ height: 1px;
1644
+ padding: 0;
1645
+ margin: -1px;
1646
+ overflow: hidden;
1647
+ clip: rect(0, 0, 0, 0);
1648
+ border: 0;
1649
+ }
1650
+
1651
+ .section-disabled {
1652
+ opacity: 0.4;
1653
+ pointer-events: none;
1654
+ }
1655
+
1656
+ .collapsible-trigger {
1657
+ display: flex;
1658
+ align-items: center;
1659
+ justify-content: space-between;
1660
+ width: 100%;
1661
+ padding: var(--space-3) 0;
1662
+ border: none;
1663
+ background: transparent;
1664
+ color: var(--text-primary);
1665
+ font-size: var(--font-base);
1666
+ font-weight: 600;
1667
+ cursor: pointer;
1668
+ font-family: var(--font-family);
1669
+ }
1670
+
1671
+ .collapsible-trigger__icon {
1672
+ transition: transform var(--transition-fast);
1673
+ }
1674
+
1675
+ .collapsible-trigger__icon--open {
1676
+ transform: rotate(180deg);
1677
+ }
1678
+
1679
+ .collapsible-content {
1680
+ overflow: hidden;
1681
+ transition: max-height var(--transition-slow);
1682
+ }
1683
+
1684
+ /* ═══════════════════════════════════════════════════════════
1685
+ Login Page
1686
+ ═══════════════════════════════════════════════════════════ */
1687
+
1688
+ .login-page {
1689
+ min-height: 100vh;
1690
+ display: flex;
1691
+ align-items: center;
1692
+ justify-content: center;
1693
+ background: var(--bg-primary);
1694
+ padding: var(--space-4);
1695
+ position: relative;
1696
+ overflow: hidden;
1697
+ }
1698
+
1699
+ .login-page::before {
1700
+ content: '';
1701
+ position: absolute;
1702
+ top: -50%;
1703
+ left: -50%;
1704
+ width: 200%;
1705
+ height: 200%;
1706
+ background: radial-gradient(
1707
+ ellipse at 30% 20%,
1708
+ rgba(16, 185, 129, 0.08) 0%,
1709
+ transparent 50%
1710
+ ),
1711
+ radial-gradient(
1712
+ ellipse at 70% 80%,
1713
+ rgba(16, 185, 129, 0.05) 0%,
1714
+ transparent 50%
1715
+ );
1716
+ pointer-events: none;
1717
+ }
1718
+
1719
+ .login-card {
1720
+ width: 100%;
1721
+ max-width: 420px;
1722
+ background: var(--surface-card);
1723
+ border: 1px solid var(--border-primary);
1724
+ border-radius: var(--radius-xl);
1725
+ padding: var(--space-10);
1726
+ box-shadow: var(--shadow-xl);
1727
+ position: relative;
1728
+ z-index: 1;
1729
+ animation: loginFadeIn 0.5s ease;
1730
+ }
1731
+
1732
+ @keyframes loginFadeIn {
1733
+ from { opacity: 0; transform: translateY(16px) scale(0.98); }
1734
+ to { opacity: 1; transform: translateY(0) scale(1); }
1735
+ }
1736
+
1737
+ .login-card__header {
1738
+ text-align: center;
1739
+ margin-bottom: var(--space-8);
1740
+ }
1741
+
1742
+ .login-card__icon {
1743
+ width: 56px;
1744
+ height: 56px;
1745
+ background: linear-gradient(135deg, var(--accent-500), var(--accent-700));
1746
+ border-radius: var(--radius-lg);
1747
+ display: flex;
1748
+ align-items: center;
1749
+ justify-content: center;
1750
+ color: white;
1751
+ margin: 0 auto var(--space-4);
1752
+ box-shadow: 0 8px 24px rgba(16, 185, 129, 0.25);
1753
+ }
1754
+
1755
+ .login-card__title {
1756
+ font-size: var(--font-2xl);
1757
+ font-weight: 800;
1758
+ letter-spacing: -0.03em;
1759
+ }
1760
+
1761
+ .login-card__title-accent {
1762
+ color: var(--accent-500);
1763
+ }
1764
+
1765
+ .login-card__subtitle {
1766
+ font-size: var(--font-sm);
1767
+ color: var(--text-tertiary);
1768
+ margin-top: var(--space-1);
1769
+ text-transform: uppercase;
1770
+ letter-spacing: 0.1em;
1771
+ font-weight: 500;
1772
+ }
1773
+
1774
+ .login-card__form {
1775
+ display: flex;
1776
+ flex-direction: column;
1777
+ gap: var(--space-5);
1778
+ }
1779
+
1780
+ .login-card__error {
1781
+ display: flex;
1782
+ align-items: center;
1783
+ gap: var(--space-2);
1784
+ padding: var(--space-3) var(--space-4);
1785
+ background: rgba(239, 68, 68, 0.1);
1786
+ border: 1px solid rgba(239, 68, 68, 0.3);
1787
+ border-radius: var(--radius-md);
1788
+ color: #ef4444;
1789
+ font-size: var(--font-sm);
1790
+ font-weight: 500;
1791
+ animation: shakeError 0.4s ease;
1792
+ }
1793
+
1794
+ @keyframes shakeError {
1795
+ 0%, 100% { transform: translateX(0); }
1796
+ 20% { transform: translateX(-6px); }
1797
+ 40% { transform: translateX(6px); }
1798
+ 60% { transform: translateX(-4px); }
1799
+ 80% { transform: translateX(4px); }
1800
+ }
1801
+
1802
+ .login-card__field {
1803
+ display: flex;
1804
+ flex-direction: column;
1805
+ gap: var(--space-2);
1806
+ }
1807
+
1808
+ .login-card__label {
1809
+ font-size: var(--font-sm);
1810
+ font-weight: 600;
1811
+ color: var(--text-secondary);
1812
+ }
1813
+
1814
+ .login-card__input-wrap {
1815
+ display: flex;
1816
+ align-items: center;
1817
+ border: 1px solid var(--border-primary);
1818
+ border-radius: var(--radius-md);
1819
+ background: var(--surface-input);
1820
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
1821
+ overflow: hidden;
1822
+ }
1823
+
1824
+ .login-card__input-wrap:focus-within {
1825
+ border-color: var(--accent-500);
1826
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
1827
+ }
1828
+
1829
+ .login-card__input-icon {
1830
+ margin-left: var(--space-3);
1831
+ color: var(--text-tertiary);
1832
+ flex-shrink: 0;
1833
+ }
1834
+
1835
+ .login-card__input {
1836
+ flex: 1;
1837
+ padding: var(--space-3) var(--space-3);
1838
+ border: none;
1839
+ background: transparent;
1840
+ color: var(--text-primary);
1841
+ font-size: var(--font-base);
1842
+ font-family: var(--font-family);
1843
+ outline: none;
1844
+ }
1845
+
1846
+ .login-card__input::placeholder {
1847
+ color: var(--text-tertiary);
1848
+ }
1849
+
1850
+ .login-card__eye {
1851
+ display: flex;
1852
+ align-items: center;
1853
+ justify-content: center;
1854
+ width: 36px;
1855
+ height: 36px;
1856
+ border: none;
1857
+ background: transparent;
1858
+ color: var(--text-tertiary);
1859
+ cursor: pointer;
1860
+ flex-shrink: 0;
1861
+ transition: color var(--transition-fast);
1862
+ }
1863
+
1864
+ .login-card__eye:hover {
1865
+ color: var(--accent-500);
1866
+ }
1867
+
1868
+ .login-card__submit {
1869
+ display: flex;
1870
+ align-items: center;
1871
+ justify-content: center;
1872
+ gap: var(--space-2);
1873
+ width: 100%;
1874
+ padding: var(--space-3) var(--space-4);
1875
+ border: none;
1876
+ border-radius: var(--radius-md);
1877
+ background: linear-gradient(135deg, var(--accent-500), var(--accent-600));
1878
+ color: white;
1879
+ font-size: var(--font-md);
1880
+ font-weight: 600;
1881
+ font-family: var(--font-family);
1882
+ cursor: pointer;
1883
+ transition: all var(--transition-fast);
1884
+ min-height: 44px;
1885
+ }
1886
+
1887
+ .login-card__submit:hover:not(:disabled) {
1888
+ background: linear-gradient(135deg, var(--accent-600), var(--accent-700));
1889
+ transform: translateY(-1px);
1890
+ box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3);
1891
+ }
1892
+
1893
+ .login-card__submit:disabled {
1894
+ opacity: 0.7;
1895
+ cursor: not-allowed;
1896
+ }
1897
+
1898
+ .login-card__spinner {
1899
+ width: 20px;
1900
+ height: 20px;
1901
+ border: 2px solid rgba(255, 255, 255, 0.3);
1902
+ border-top-color: white;
1903
+ border-radius: 50%;
1904
+ animation: spin 0.7s linear infinite;
1905
+ }
1906
+
1907
+ .login-card__footer {
1908
+ text-align: center;
1909
+ font-size: var(--font-xs);
1910
+ color: var(--text-tertiary);
1911
+ margin-top: var(--space-6);
1912
+ text-transform: uppercase;
1913
+ letter-spacing: 0.06em;
1914
+ }
1915
+
1916
+ /* ── Logged-in user pill in navbar ── */
1917
+
1918
+ .navbar__user {
1919
+ display: flex;
1920
+ align-items: center;
1921
+ gap: var(--space-2);
1922
+ padding: var(--space-1) var(--space-3);
1923
+ border: 1px solid var(--border-primary);
1924
+ border-radius: var(--radius-full);
1925
+ font-size: var(--font-xs);
1926
+ color: var(--text-secondary);
1927
+ font-weight: 500;
1928
+ }
1929
+
1930
+ .navbar__user-name {
1931
+ color: var(--accent-500);
1932
+ font-weight: 600;
1933
+ }
1934
+
1935
+ /* ── Loading Container ── */
1936
+
1937
+ .loading-container {
1938
+ display: flex;
1939
+ flex-direction: column;
1940
+ align-items: center;
1941
+ gap: var(--space-4);
1942
+ }
1943
+
1944
+ .loading-spinner {
1945
+ width: 40px;
1946
+ height: 40px;
1947
+ border: 3px solid var(--border-primary);
1948
+ border-top-color: var(--accent-500);
1949
+ border-radius: 50%;
1950
+ animation: spin 0.8s linear infinite;
1951
+ }
1952
+
1953
+ .loading-text {
1954
+ font-size: var(--font-sm);
1955
+ color: var(--text-tertiary);
1956
+ font-weight: 500;
1957
+ }
1958
+
1959
+ @keyframes spin {
1960
+ to { transform: rotate(360deg); }
1961
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App.jsx'
4
+
5
+ createRoot(document.getElementById('root')).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ )
frontend/vite.config.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ '/api': {
10
+ target: 'http://localhost:8000',
11
+ changeOrigin: true,
12
+ },
13
+ },
14
+ },
15
+ })