diff --git a/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-BJx1p784.css b/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-BJx1p784.css index 6a04f87ab..56abff8af 100644 --- a/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-BJx1p784.css +++ b/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-BJx1p784.css @@ -1 +1,1582 @@ -:root{--bg-diagram: #f9f8f6;--bg-card: #ffffff;--grid-color: rgba(13, 115, 119, .08);--accent: #0d7377;--accent-light: rgba(13, 115, 119, .1);--accent-mid: rgba(13, 115, 119, .2);--text-dark: #1e3a3a;--text-mid: #4a6363;--text-light: #7a9494;--border: #d4dede;--border-accent: rgba(13, 115, 119, .4);--warning: #b45309;--warning-bg: rgba(180, 83, 9, .08)}*{margin:0;padding:0;box-sizing:border-box}body{font-family:"Source Serif 4",Georgia,serif;line-height:1.9;color:#2d3748;background:#f7f8fa}.scrolly-container{max-width:1400px;margin:0 auto;padding:40px 20px}.article-wrapper{background:#fff;padding:80px 120px;box-shadow:0 4px 40px #0000000f;border-radius:3px}h1{font-family:"Source Serif 4",Georgia,serif;font-size:3rem;font-weight:700;color:#1a202c;margin-bottom:20px;line-height:1.2;letter-spacing:-.02em}.subtitle{font-size:1.4rem;color:#64748b;font-style:italic;margin-bottom:50px;padding-bottom:50px;border-bottom:1px solid #e2e8f0}h2{font-family:"Source Serif 4",Georgia,serif;font-size:2rem;font-weight:700;color:#1a202c;margin-top:70px;margin-bottom:30px;letter-spacing:-.01em}h3{font-size:1.4rem;font-weight:600;color:#2d3748;margin-top:40px;margin-bottom:20px}p{margin-bottom:24px;font-size:1.2rem;color:#4a5568}ul,ol{margin-bottom:24px;padding-left:32px}li{margin-bottom:12px;font-size:1.2rem;color:#4a5568}strong{color:#1a202c;font-weight:600}code{font-family:JetBrains Mono,monospace;background:#f1f5f9;padding:3px 8px;border-radius:4px;font-size:.9em;color:#0f766e}pre{background:#1e2d2f;color:#e8f0f0;padding:28px 32px;border-radius:8px;overflow-x:auto;margin-bottom:30px;font-family:JetBrains Mono,monospace;font-size:1rem;line-height:1.7;border:1px solid #2d4244}pre code{background:none;padding:0;color:inherit}hr{border:none;border-top:1px solid #e2e8f0;margin:60px 0}a{color:#0f766e;text-decoration:none;border-bottom:1px solid rgba(15,118,110,.3);transition:border-color .2s}a:hover{border-color:#0f766e}.footer{margin-top:60px;padding-top:40px;border-top:1px solid #e2e8f0;font-size:1.1rem;color:#64748b;font-style:italic}.iteration-container{margin:30px 0}.iteration-hint{text-align:center;color:var(--text-light);font-family:JetBrains Mono,monospace;font-size:.75rem;margin-bottom:12px}.iteration-cards{display:flex;gap:12px;margin-bottom:20px}.iteration-card{flex:1;background:var(--bg-card);border:2px solid var(--border);border-radius:10px;padding:16px 14px;text-align:center;cursor:pointer;transition:all .25s ease;position:relative}.iteration-card:after{content:"";position:absolute;bottom:-12px;left:50%;transform:translate(-50%) scaleY(0);width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent;border-top:10px solid var(--accent);transition:transform .2s ease;transform-origin:top}.iteration-card:hover{border-color:var(--accent);transform:translateY(-4px);box-shadow:0 8px 30px #0d737726}.iteration-card.active{background:var(--accent);border-color:var(--accent);transform:translateY(-4px);box-shadow:0 8px 30px #0d737733}.iteration-card.active:after{transform:translate(-50%) scaleY(1)}.iteration-num{display:flex;align-items:center;justify-content:center;width:36px;height:36px;background:var(--accent);color:#fff;border-radius:50%;font-family:JetBrains Mono,monospace;font-size:1rem;font-weight:700;margin:0 auto 10px}.iteration-card.active .iteration-num{background:#fff;color:var(--accent)}.iteration-title{font-family:JetBrains Mono,monospace;font-size:.9rem;font-weight:600;color:var(--text-dark);margin-bottom:4px}.iteration-card.active .iteration-title{color:#fff}.iteration-subtitle{font-family:"Source Serif 4",Georgia,serif;font-size:.8rem;color:var(--text-light);line-height:1.3}.iteration-card.active .iteration-subtitle{color:#fffc}.iteration-connector{display:flex;align-items:center;color:var(--text-light);font-size:1.2rem;padding:0 2px;margin-top:20px}.iteration-panel{background:var(--bg-diagram);border:2px solid var(--accent);border-radius:12px;padding:24px 32px;overflow:hidden}.iteration-panel p{margin:10px 0;line-height:1.7;font-size:1.05rem}.scrollytelling-container{display:flex;gap:40px;position:relative;margin:40px 0}.scrolly-narrative{flex:1;min-width:0}.scrolly-sticky{flex:0 0 420px;position:sticky;top:40px;height:fit-content;align-self:flex-start}.narrative-step{min-height:90vh;padding:40px 0;opacity:.3;transition:opacity .4s ease}.narrative-step.active{opacity:1}.narrative-step:first-child{padding-top:0}.narrative-step:last-child{min-height:auto;padding-bottom:100px}.step-header{display:flex;align-items:baseline;gap:12px;margin-bottom:20px}.step-number{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:var(--accent);color:#fff;border-radius:50%;font-family:JetBrains Mono,monospace;font-size:.95rem;font-weight:700;flex-shrink:0}.step-title{font-family:JetBrains Mono,monospace;font-size:1.3rem;font-weight:600;color:var(--text-dark)}.step-subtitle{font-size:.9rem;color:var(--text-light);margin-left:8px}.step-diagram{background:var(--bg-diagram);background-image:radial-gradient(circle,var(--grid-color) 1px,transparent 1px);background-size:20px 20px;border-radius:12px;padding:32px 40px;margin-bottom:28px;border:1px solid var(--border);display:flex;justify-content:center;align-items:center;overflow-x:auto}.full-width-flow-svg{width:100%;max-width:100%;height:auto;min-height:160px}.full-width-flow-svg .network-node{fill:var(--bg-card);stroke:var(--accent);stroke-width:2}.full-width-flow-svg .network-line{stroke:var(--text-mid);stroke-width:2;stroke-dasharray:6 4;fill:none}.full-width-flow-svg .network-label{font-family:JetBrains Mono,monospace;font-size:13px;fill:var(--text-mid);text-anchor:middle}.full-width-flow-svg .flow-arrow{fill:var(--accent)}.full-width-flow-svg .svg-icon{color:var(--text-mid);pointer-events:none}.full-width-flow-svg .loop-indicator{fill:#0d737708;stroke:var(--accent);stroke-width:2.5;stroke-dasharray:12 6;opacity:.7;animation:rotateLoop 6s linear infinite}.step-content{padding-left:0}.step-content p{font-size:1.1rem;line-height:1.8;margin-bottom:20px}.wins-shortcomings{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:24px 0}.wins,.shortcomings{padding:20px;border-radius:10px}.wins{background:#10b98114;border:1px solid rgba(16,185,129,.2)}.shortcomings{background:#ef44440f;border:1px solid rgba(239,68,68,.15)}.wins-title,.shortcomings-title{font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-bottom:12px;display:flex;align-items:center;gap:8px}.wins-title{color:#059669}.shortcomings-title{color:#dc2626}.wins ul,.shortcomings ul{list-style:none;padding:0;margin:0}.wins li,.shortcomings li{font-size:.95rem;padding:6px 0 6px 20px;position:relative;margin:0}.wins li:before{content:"✓";position:absolute;left:0;color:#059669;font-weight:600}.shortcomings li:before{content:"✗";position:absolute;left:0;color:#dc2626;font-weight:600}.insight-box{background:linear-gradient(135deg,var(--accent-light) 0%,rgba(13,115,119,.05) 100%);border-left:4px solid var(--accent);padding:20px 24px;margin:28px 0;border-radius:0 10px 10px 0}.insight-box p{margin:0;font-size:1.05rem;color:var(--text-dark);font-style:italic}.insight-label{font-family:JetBrains Mono,monospace;font-size:.7rem;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:.1em;margin-bottom:8px}.inline-code-example{background:#1e2d2f;border-radius:8px;overflow:hidden;margin:20px 0;border:1px solid #2d4244}.inline-code-title{background:#253538;padding:10px 16px;font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #2d4244}.inline-code-block{background:transparent;border:none;padding:16px 20px;margin:0;font-family:JetBrains Mono,monospace;font-size:.85rem;line-height:1.6;color:#e8f0f0;overflow-x:auto}.inline-code-block code{background:none;padding:0;color:inherit;font-size:inherit}.inline-diagram{background:var(--bg-diagram);border-radius:10px;overflow:hidden;margin:0 0 24px;border:1px solid var(--border)}.inline-diagram-title{background:var(--accent-light);padding:10px 16px;font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border)}.inline-diagram-content{padding:16px 20px;display:flex;justify-content:center;align-items:center}.inline-flow-svg{width:100%;max-width:500px;height:auto}.example-panel{background:var(--bg-card);border-radius:12px;overflow:hidden;box-shadow:0 1px 3px #0d737714,0 8px 24px #0d73771f;border:2px solid var(--accent)}.example-panel.diagram-only{background:var(--bg-diagram)}.example-header{background:var(--bg-diagram);padding:16px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}.example-panel.diagram-only .example-header{background:var(--bg-diagram);border-bottom:1px solid var(--border)}.example-title{font-family:JetBrains Mono,monospace;font-size:.9rem;font-weight:600;color:var(--text-dark);letter-spacing:.02em}.example-badge{background:var(--accent);color:#fff;padding:5px 12px;border-radius:6px;font-family:JetBrains Mono,monospace;font-size:.7rem;font-weight:600;letter-spacing:.03em}.example-tabs{display:flex;gap:0;background:var(--bg-card);border-bottom:1px solid var(--border)}.example-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:12px 16px;background:transparent;border:none;cursor:pointer;font-family:JetBrains Mono,monospace;font-size:.75rem;color:var(--text-mid);transition:all .2s ease;position:relative}.example-tab:hover{background:var(--accent-light);color:var(--text-dark)}.example-tab.active{background:var(--accent-light);color:var(--accent)}.example-tab.active:after{content:"";position:absolute;bottom:-1px;left:0;right:0;height:2px;background:var(--accent)}.example-tab .tab-icon{font-size:.9rem}.example-tab .tab-label{font-weight:500}.example-file-status.success{background:#10b98126;color:#059669}.example-file-status.warning{background:#b4530926;color:#b45309}.example-body{padding:20px 24px 24px;min-height:400px;background:var(--bg-diagram)}.example-file-header{font-family:JetBrains Mono,monospace;font-size:.9rem;font-weight:600;color:var(--text-dark);margin-bottom:20px;padding-bottom:12px;border-bottom:1px solid var(--border)}.example-panel.diagram-only .example-body{padding:20px;min-height:350px;display:flex;flex-direction:column}.diagram-title{font-family:JetBrains Mono,monospace;font-size:1.1rem;font-weight:600;color:var(--text-dark);text-align:center;margin-bottom:20px;padding-bottom:12px;border-bottom:1px solid var(--border)}.diagram-container{flex:1;display:flex;align-items:center;justify-content:center;padding:10px}.diagram-container .flow-diagram-svg{width:100%;height:auto;max-height:320px}.example-section{margin-bottom:2px}.example-section:last-child{margin-bottom:0}.example-section-title{font-family:JetBrains Mono,monospace;font-size:.65rem;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:.12em;margin-top:0;margin-bottom:4px;display:flex;align-items:center;gap:8px}.example-section-title:after{content:"";flex:1;height:1px;background:var(--border)}.example-code{background:linear-gradient(135deg,#f5f7f7 0%,var(--bg-diagram) 100%);border-radius:8px;padding:16px 18px;font-family:JetBrains Mono,monospace;font-size:.82rem;line-height:1.7;color:var(--text-dark);overflow-x:auto;border:1px solid var(--border);box-shadow:inset 0 1px 3px #0000000a}.example-code .comment{color:var(--text-light);font-style:italic}.example-code .keyword{color:var(--accent);font-weight:500}.example-code .string{color:#047857}.example-code .number{color:#b45309}.example-code .error{color:#dc2626}.example-code .success{color:#059669}.example-file{display:flex;align-items:center;gap:10px;padding:12px 16px;background:var(--bg-card);border-radius:8px;margin-bottom:12px;border:1px solid var(--border);box-shadow:0 1px 3px #0000000a}.example-file-icon{font-size:1.1rem}.example-file-name{font-family:JetBrains Mono,monospace;font-size:.85rem;font-weight:500;color:var(--text-dark)}.example-file-status{margin-left:auto;font-size:.75rem;padding:2px 8px;border-radius:4px;font-family:JetBrains Mono,monospace}.example-file-status.created{background:#10b98126;color:#059669}.example-file-status.error{background:#dc26261f;color:#dc2626}.example-output{background:var(--bg-card);border-radius:8px;padding:16px 18px;font-family:JetBrains Mono,monospace;font-size:.8rem;border:1px solid var(--border)}.example-output-line{display:flex;align-items:flex-start;gap:10px;color:var(--text-mid);transition:background .15s ease;margin:0 -8px;padding:2px 8px;border-radius:4px}.example-output-line:hover{background:var(--accent-light)}.example-output-line .icon{flex-shrink:0;font-weight:600}.example-output-line.error{color:#dc2626}.example-output-line.error:hover{background:#dc262614}.example-output-line.success{color:#059669}.example-output-line.success:hover{background:#05966914}.example-output-line.warning{color:#b45309}.example-output-line.warning:hover{background:#b4530914}.example-diagram{background:var(--bg-diagram);border-radius:8px;padding:20px;margin-top:16px}.example-diagram svg{width:100%;height:auto}.example-diagram-container{background:var(--bg-diagram);border-radius:8px;padding:16px;border:1px solid var(--border)}.flow-diagram-svg{width:100%;height:auto;display:block}@media(max-width:1100px){.scrollytelling-container{flex-direction:column}.scrolly-sticky{position:relative;top:0;flex:none;width:100%;order:-1;margin-bottom:30px}.narrative-step{min-height:auto;padding:30px 0;opacity:1}}@media(max-width:768px){.wins-shortcomings{grid-template-columns:1fr}}.principles-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin:40px 0}.principle-card{background:var(--bg-card);border:2px solid var(--border);border-radius:12px;padding:32px 28px;transition:all .25s ease;position:relative;overflow:hidden}.principle-card:before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .3s ease}.principle-card:hover{border-color:var(--accent);transform:translateY(-6px);box-shadow:0 12px 40px #0d73771f}.principle-card:hover:before{transform:scaleX(1)}.principle-icon{width:56px;height:56px;background:var(--accent-light);border-radius:12px;display:flex;align-items:center;justify-content:center;margin-bottom:20px;color:var(--accent)}.principle-icon svg{width:28px;height:28px}.principle-title{font-family:JetBrains Mono,monospace;font-size:1.1rem;font-weight:600;color:var(--text-dark);margin-bottom:12px}.principle-desc{font-family:"Source Serif 4",Georgia,serif;font-size:1.05rem;color:var(--text-mid);line-height:1.7;margin:0}.principles-bottom{display:grid;grid-template-columns:repeat(2,1fr);gap:24px;max-width:820px;margin:0 auto}.workflow-timeline{background:var(--bg-diagram);background-image:radial-gradient(circle,var(--grid-color) 1px,transparent 1px);background-size:20px 20px;border-radius:16px;padding:50px 40px;margin:40px 0;border:1px solid var(--border)}.workflow-header{text-align:center;margin-bottom:40px}.workflow-command{display:inline-block;background:var(--accent);color:#fff;padding:14px 32px;border-radius:10px;font-family:JetBrains Mono,monospace;font-size:1.2rem;font-weight:600}.workflow-command-label{font-family:JetBrains Mono,monospace;font-size:.7rem;color:var(--text-light);text-transform:uppercase;letter-spacing:.15em;margin-bottom:10px}.timeline-phases{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;position:relative}.timeline-phases:before{content:"";position:absolute;top:25px;left:12%;right:12%;height:3px;background:linear-gradient(90deg,var(--accent) 0%,var(--accent) 100%);z-index:0}.timeline-phase{position:relative;z-index:1}.timeline-phase-header{text-align:center;margin-bottom:16px;transition:transform .2s ease}.timeline-phase-header:hover{transform:scale(1.05)}.timeline-phase-header:hover .timeline-phase-num{box-shadow:0 6px 20px #0d737766}.timeline-phase-num{display:inline-flex;align-items:center;justify-content:center;width:50px;height:50px;background:var(--accent);color:#fff;border-radius:50%;font-family:JetBrains Mono,monospace;font-size:1.2rem;font-weight:700;margin-bottom:10px;box-shadow:0 4px 15px #0d73774d}.timeline-phase-title{font-family:JetBrains Mono,monospace;font-size:1rem;font-weight:600;color:var(--text-dark)}.timeline-phase-chevron{display:inline-flex;align-items:center;justify-content:center;margin-top:8px;color:var(--accent);transition:transform .3s ease}.timeline-phase-chevron.expanded{transform:rotate(180deg)}.timeline-phase-card{background:var(--bg-card);border:2px solid var(--border);border-radius:12px;padding:24px 20px;min-height:280px;transition:all .25s ease}.timeline-phase-card:hover{border-color:var(--accent);box-shadow:0 8px 30px #0d73771a}.timeline-agents{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px}.timeline-agent{background:var(--accent-light);color:var(--accent);padding:4px 10px;border-radius:4px;font-family:JetBrains Mono,monospace;font-size:.7rem;font-weight:500}.timeline-phase-desc{font-size:.95rem;color:var(--text-mid);line-height:1.6;margin:0}.timeline-phase-desc strong{color:var(--text-dark)}.timeline-steps{list-style:none;padding:0;margin:12px 0 0}.timeline-steps li{font-size:.9rem;color:var(--text-mid);padding:4px 0 4px 18px;position:relative}.timeline-steps li:before{content:"→";position:absolute;left:0;color:var(--accent)}.results-section{margin:40px 0}.results-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-bottom:40px}.stat-card{background:linear-gradient(135deg,var(--accent) 0%,#0a5d60 100%);border-radius:16px;padding:36px 28px;text-align:center;color:#fff;position:relative;overflow:hidden}.stat-card:before{content:"";position:absolute;top:-50%;right:-50%;width:100%;height:100%;background:radial-gradient(circle,rgba(255,255,255,.1) 0%,transparent 60%)}.stat-number{font-family:JetBrains Mono,monospace;font-size:3rem;font-weight:700;margin-bottom:8px;position:relative}.stat-label{font-family:"Source Serif 4",Georgia,serif;font-size:1.1rem;opacity:.9;position:relative}.stat-detail{font-size:.85rem;opacity:.75;margin-top:8px;position:relative}.results-description{background:var(--bg-diagram);border-radius:12px;padding:28px 32px;border-left:4px solid var(--accent)}.results-description p{margin:0;font-size:1.1rem}.terminal-container{background:#1a1a2e;border-radius:12px;overflow:hidden;margin:30px 0;box-shadow:0 8px 30px #0003}.terminal-header{background:#16162a;padding:12px 16px;display:flex;align-items:center;gap:8px}.terminal-dot{width:12px;height:12px;border-radius:50%}.terminal-dot.red{background:#ff5f57}.terminal-dot.yellow{background:#febc2e}.terminal-dot.green{background:#28c840}.terminal-title{margin-left:12px;font-family:JetBrains Mono,monospace;font-size:.8rem;color:#666}.terminal-body{padding:24px 28px}.terminal-body pre{background:transparent;border:none;padding:0;margin:0;color:#e8f0f0}.terminal-body code{background:transparent;color:inherit;font-size:1rem}.terminal-comment{color:#6b7280}.terminal-command{color:#10b981}.next-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:28px;margin:40px 0}.next-card{background:var(--bg-card);border:2px solid var(--border);border-radius:12px;padding:32px 28px;position:relative;transition:all .25s ease}.next-card:hover{border-color:var(--accent);transform:translateY(-4px);box-shadow:0 12px 40px #0d73771a}.next-card-badge{display:inline-block;background:var(--accent-light);color:var(--accent);padding:6px 14px;border-radius:20px;font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:16px}.next-card-title{font-family:JetBrains Mono,monospace;font-size:1.2rem;font-weight:600;color:var(--text-dark);margin-bottom:14px}.next-card-desc{font-size:1.05rem;color:var(--text-mid);line-height:1.7;margin:0}.agent-flow-container{background:var(--bg-diagram);background-image:radial-gradient(circle,var(--grid-color) 1px,transparent 1px);background-size:20px 20px;border-radius:12px;padding:60px 40px 50px;margin:50px 0;position:relative;border:1px solid var(--border);box-shadow:inset 0 2px 8px #00000008;overflow-x:auto}.agent-flow-container:before{content:"AGENT FLOW";position:absolute;top:18px;left:24px;font-family:JetBrains Mono,monospace;font-size:.7rem;font-weight:600;letter-spacing:.2em;color:var(--text-light);text-transform:uppercase}.agent-flow-svg{display:block;margin:0 auto;max-width:100%;height:auto}.agent-flow-svg .network-node{fill:var(--bg-card);stroke:var(--accent);stroke-width:2}.agent-flow-svg .network-line{stroke:var(--text-mid);stroke-width:2;stroke-dasharray:6 4;fill:none}.agent-flow-svg .network-line.thick{stroke-width:2.5}.agent-flow-svg .network-label{font-family:JetBrains Mono,monospace;font-size:13px;fill:var(--text-mid);text-anchor:middle}.agent-flow-svg .group-label{font-family:JetBrains Mono,monospace;font-size:14px;font-weight:600;fill:var(--accent);text-anchor:middle;text-transform:uppercase;letter-spacing:.05em}.agent-flow-svg .flow-arrow{fill:var(--accent)}.agent-flow-svg .svg-icon{color:var(--text-mid);pointer-events:none}.agent-flow-svg .loop-indicator{fill:#0d737708;stroke:var(--accent);stroke-width:2.5;stroke-dasharray:12 6;opacity:.7;animation:rotateLoop 6s linear infinite}@keyframes rotateLoop{0%{stroke-dashoffset:0}to{stroke-dashoffset:-72}}.agent-flow-svg .loop-label{font-family:JetBrains Mono,monospace;font-size:13px;font-weight:600;font-style:italic;fill:#1a202c;text-anchor:middle;letter-spacing:.03em}.stage-diagram{padding:30px 20px 25px;margin:25px auto;max-width:100%;overflow-x:auto}.stage-diagram svg{display:block;margin:0 auto;width:100%;height:auto}.stages-diagram-container{background:var(--bg-diagram);border-radius:12px;padding:20px;margin:30px 0;border:1px solid var(--border);overflow-x:auto}.stages-diagram-svg{display:block;margin:0 auto;width:100%;max-width:1100px;height:auto;min-height:400px}.stages-diagram-svg .network-node{fill:var(--bg-card);stroke:var(--accent);stroke-width:2}.stages-diagram-svg .network-line{stroke:var(--text-mid);stroke-width:1.5;stroke-dasharray:5 3;fill:none}.stages-diagram-svg .network-label{font-family:JetBrains Mono,monospace;font-size:11px;fill:var(--text-mid);text-anchor:middle}.stages-diagram-svg .svg-icon{color:var(--text-mid);pointer-events:none}.stages-diagram-svg .loop-indicator{fill:#0d737708;stroke:var(--accent);stroke-width:2.5;stroke-dasharray:12 6;opacity:.6;animation:rotateLoop 6s linear infinite}@media(max-width:1200px){.article-wrapper{padding:60px}h1{font-size:2.4rem}.principles-grid{grid-template-columns:repeat(2,1fr)}.principles-bottom{grid-template-columns:1fr;max-width:100%}.timeline-phases{grid-template-columns:repeat(2,1fr)}.timeline-phases:before{display:none}}@media(max-width:768px){.article-wrapper{padding:30px 25px}h1{font-size:1.8rem}.iteration-cards{flex-direction:column}.iteration-connector{display:none}.principles-grid,.timeline-phases,.results-stats,.next-cards{grid-template-columns:1fr}}.step-diagram.clickable{cursor:zoom-in;position:relative;transition:all .2s ease}.step-diagram.clickable:hover{border-color:var(--accent);box-shadow:0 4px 20px #0d737726}.step-diagram.clickable:hover .expand-hint{opacity:1}.expand-hint{position:absolute;bottom:12px;right:16px;background:var(--accent);color:#fff;padding:6px 12px;border-radius:6px;font-family:JetBrains Mono,monospace;font-size:.7rem;font-weight:500;opacity:0;transition:opacity .2s ease;pointer-events:none}.diagram-modal-overlay{position:fixed;inset:0;background:#000000d9;display:flex;align-items:center;justify-content:center;z-index:9999;padding:40px;cursor:zoom-out}.diagram-modal-content{background:var(--bg-diagram);background-image:radial-gradient(circle,var(--grid-color) 1px,transparent 1px);background-size:20px 20px;border-radius:16px;padding:32px 40px 40px;max-width:95vw;max-height:90vh;overflow:auto;position:relative;cursor:default;box-shadow:0 20px 60px #0006;border:2px solid var(--accent)}.diagram-modal-close{position:absolute;top:16px;right:20px;background:var(--accent);color:#fff;border:none;width:36px;height:36px;border-radius:50%;font-size:24px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease}.diagram-modal-close:hover{background:#0a5d60;transform:scale(1.1)}.diagram-modal-title{font-family:JetBrains Mono,monospace;font-size:1.2rem;font-weight:600;color:var(--text-dark);margin-bottom:24px;padding-right:50px}.diagram-modal-diagram{display:flex;justify-content:center;align-items:center;min-width:800px}.diagram-modal-diagram svg{width:100%;height:auto;max-height:70vh}@media(max-width:900px){.diagram-modal-overlay{padding:20px}.diagram-modal-content{padding:24px}.diagram-modal-diagram{min-width:auto}} +:root { + --bg-diagram: #f9f8f6; + --bg-card: #ffffff; + --grid-color: rgba(13, 115, 119, 0.08); + --accent: #0d7377; + --accent-light: rgba(13, 115, 119, 0.1); + --accent-mid: rgba(13, 115, 119, 0.2); + --text-dark: #1e3a3a; + --text-mid: #4a6363; + --text-light: #7a9494; + --border: #d4dede; + --border-accent: rgba(13, 115, 119, 0.4); + --warning: #b45309; + --warning-bg: rgba(180, 83, 9, 0.08); +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +body { + font-family: 'Source Serif 4', Georgia, serif; + line-height: 1.9; + color: #2d3748; + background: #f7f8fa; +} +.scrolly-container { + max-width: 1400px; + margin: 0 auto; + padding: 40px 20px; +} +.article-wrapper { + background: #fff; + padding: 80px 120px; + box-shadow: 0 4px 40px #0000000f; + border-radius: 3px; +} +h1 { + font-family: 'Source Serif 4', Georgia, serif; + font-size: 3rem; + font-weight: 700; + color: #1a202c; + margin-bottom: 20px; + line-height: 1.2; + letter-spacing: -0.02em; +} +.subtitle { + font-size: 1.4rem; + color: #64748b; + font-style: italic; + margin-bottom: 50px; + padding-bottom: 50px; + border-bottom: 1px solid #e2e8f0; +} +h2 { + font-family: 'Source Serif 4', Georgia, serif; + font-size: 2rem; + font-weight: 700; + color: #1a202c; + margin-top: 70px; + margin-bottom: 30px; + letter-spacing: -0.01em; +} +h3 { + font-size: 1.4rem; + font-weight: 600; + color: #2d3748; + margin-top: 40px; + margin-bottom: 20px; +} +p { + margin-bottom: 24px; + font-size: 1.2rem; + color: #4a5568; +} +ul, +ol { + margin-bottom: 24px; + padding-left: 32px; +} +li { + margin-bottom: 12px; + font-size: 1.2rem; + color: #4a5568; +} +strong { + color: #1a202c; + font-weight: 600; +} +code { + font-family: + JetBrains Mono, + monospace; + background: #f1f5f9; + padding: 3px 8px; + border-radius: 4px; + font-size: 0.9em; + color: #0f766e; +} +pre { + background: #1e2d2f; + color: #e8f0f0; + padding: 28px 32px; + border-radius: 8px; + overflow-x: auto; + margin-bottom: 30px; + font-family: + JetBrains Mono, + monospace; + font-size: 1rem; + line-height: 1.7; + border: 1px solid #2d4244; +} +pre code { + background: none; + padding: 0; + color: inherit; +} +hr { + border: none; + border-top: 1px solid #e2e8f0; + margin: 60px 0; +} +a { + color: #0f766e; + text-decoration: none; + border-bottom: 1px solid rgba(15, 118, 110, 0.3); + transition: border-color 0.2s; +} +a:hover { + border-color: #0f766e; +} +.footer { + margin-top: 60px; + padding-top: 40px; + border-top: 1px solid #e2e8f0; + font-size: 1.1rem; + color: #64748b; + font-style: italic; +} +.iteration-container { + margin: 30px 0; +} +.iteration-hint { + text-align: center; + color: var(--text-light); + font-family: + JetBrains Mono, + monospace; + font-size: 0.75rem; + margin-bottom: 12px; +} +.iteration-cards { + display: flex; + gap: 12px; + margin-bottom: 20px; +} +.iteration-card { + flex: 1; + background: var(--bg-card); + border: 2px solid var(--border); + border-radius: 10px; + padding: 16px 14px; + text-align: center; + cursor: pointer; + transition: all 0.25s ease; + position: relative; +} +.iteration-card:after { + content: ''; + position: absolute; + bottom: -12px; + left: 50%; + transform: translate(-50%) scaleY(0); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid var(--accent); + transition: transform 0.2s ease; + transform-origin: top; +} +.iteration-card:hover { + border-color: var(--accent); + transform: translateY(-4px); + box-shadow: 0 8px 30px #0d737726; +} +.iteration-card.active { + background: var(--accent); + border-color: var(--accent); + transform: translateY(-4px); + box-shadow: 0 8px 30px #0d737733; +} +.iteration-card.active:after { + transform: translate(-50%) scaleY(1); +} +.iteration-num { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: var(--accent); + color: #fff; + border-radius: 50%; + font-family: + JetBrains Mono, + monospace; + font-size: 1rem; + font-weight: 700; + margin: 0 auto 10px; +} +.iteration-card.active .iteration-num { + background: #fff; + color: var(--accent); +} +.iteration-title { + font-family: + JetBrains Mono, + monospace; + font-size: 0.9rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 4px; +} +.iteration-card.active .iteration-title { + color: #fff; +} +.iteration-subtitle { + font-family: 'Source Serif 4', Georgia, serif; + font-size: 0.8rem; + color: var(--text-light); + line-height: 1.3; +} +.iteration-card.active .iteration-subtitle { + color: #fffc; +} +.iteration-connector { + display: flex; + align-items: center; + color: var(--text-light); + font-size: 1.2rem; + padding: 0 2px; + margin-top: 20px; +} +.iteration-panel { + background: var(--bg-diagram); + border: 2px solid var(--accent); + border-radius: 12px; + padding: 24px 32px; + overflow: hidden; +} +.iteration-panel p { + margin: 10px 0; + line-height: 1.7; + font-size: 1.05rem; +} +.scrollytelling-container { + display: flex; + gap: 40px; + position: relative; + margin: 40px 0; +} +.scrolly-narrative { + flex: 1; + min-width: 0; +} +.scrolly-sticky { + flex: 0 0 420px; + position: sticky; + top: 40px; + height: fit-content; + align-self: flex-start; +} +.narrative-step { + min-height: 90vh; + padding: 40px 0; + opacity: 0.3; + transition: opacity 0.4s ease; +} +.narrative-step.active { + opacity: 1; +} +.narrative-step:first-child { + padding-top: 0; +} +.narrative-step:last-child { + min-height: auto; + padding-bottom: 100px; +} +.step-header { + display: flex; + align-items: baseline; + gap: 12px; + margin-bottom: 20px; +} +.step-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: var(--accent); + color: #fff; + border-radius: 50%; + font-family: + JetBrains Mono, + monospace; + font-size: 0.95rem; + font-weight: 700; + flex-shrink: 0; +} +.step-title { + font-family: + JetBrains Mono, + monospace; + font-size: 1.3rem; + font-weight: 600; + color: var(--text-dark); +} +.step-subtitle { + font-size: 0.9rem; + color: var(--text-light); + margin-left: 8px; +} +.step-diagram { + background: var(--bg-diagram); + background-image: radial-gradient(circle, var(--grid-color) 1px, transparent 1px); + background-size: 20px 20px; + border-radius: 12px; + padding: 32px 40px; + margin-bottom: 28px; + border: 1px solid var(--border); + display: flex; + justify-content: center; + align-items: center; + overflow-x: auto; +} +.full-width-flow-svg { + width: 100%; + max-width: 100%; + height: auto; + min-height: 160px; +} +.full-width-flow-svg .network-node { + fill: var(--bg-card); + stroke: var(--accent); + stroke-width: 2; +} +.full-width-flow-svg .network-line { + stroke: var(--text-mid); + stroke-width: 2; + stroke-dasharray: 6 4; + fill: none; +} +.full-width-flow-svg .network-label { + font-family: + JetBrains Mono, + monospace; + font-size: 13px; + fill: var(--text-mid); + text-anchor: middle; +} +.full-width-flow-svg .flow-arrow { + fill: var(--accent); +} +.full-width-flow-svg .svg-icon { + color: var(--text-mid); + pointer-events: none; +} +.full-width-flow-svg .loop-indicator { + fill: #0d737708; + stroke: var(--accent); + stroke-width: 2.5; + stroke-dasharray: 12 6; + opacity: 0.7; + animation: rotateLoop 6s linear infinite; +} +.step-content { + padding-left: 0; +} +.step-content p { + font-size: 1.1rem; + line-height: 1.8; + margin-bottom: 20px; +} +.wins-shortcomings { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin: 24px 0; +} +.wins, +.shortcomings { + padding: 20px; + border-radius: 10px; +} +.wins { + background: #10b98114; + border: 1px solid rgba(16, 185, 129, 0.2); +} +.shortcomings { + background: #ef44440f; + border: 1px solid rgba(239, 68, 68, 0.15); +} +.wins-title, +.shortcomings-title { + font-family: + JetBrains Mono, + monospace; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} +.wins-title { + color: #059669; +} +.shortcomings-title { + color: #dc2626; +} +.wins ul, +.shortcomings ul { + list-style: none; + padding: 0; + margin: 0; +} +.wins li, +.shortcomings li { + font-size: 0.95rem; + padding: 6px 0 6px 20px; + position: relative; + margin: 0; +} +.wins li:before { + content: '✓'; + position: absolute; + left: 0; + color: #059669; + font-weight: 600; +} +.shortcomings li:before { + content: '✗'; + position: absolute; + left: 0; + color: #dc2626; + font-weight: 600; +} +.insight-box { + background: linear-gradient(135deg, var(--accent-light) 0%, rgba(13, 115, 119, 0.05) 100%); + border-left: 4px solid var(--accent); + padding: 20px 24px; + margin: 28px 0; + border-radius: 0 10px 10px 0; +} +.insight-box p { + margin: 0; + font-size: 1.05rem; + color: var(--text-dark); + font-style: italic; +} +.insight-label { + font-family: + JetBrains Mono, + monospace; + font-size: 0.7rem; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 8px; +} +.inline-code-example { + background: #1e2d2f; + border-radius: 8px; + overflow: hidden; + margin: 20px 0; + border: 1px solid #2d4244; +} +.inline-code-title { + background: #253538; + padding: 10px 16px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.75rem; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #2d4244; +} +.inline-code-block { + background: transparent; + border: none; + padding: 16px 20px; + margin: 0; + font-family: + JetBrains Mono, + monospace; + font-size: 0.85rem; + line-height: 1.6; + color: #e8f0f0; + overflow-x: auto; +} +.inline-code-block code { + background: none; + padding: 0; + color: inherit; + font-size: inherit; +} +.inline-diagram { + background: var(--bg-diagram); + border-radius: 10px; + overflow: hidden; + margin: 0 0 24px; + border: 1px solid var(--border); +} +.inline-diagram-title { + background: var(--accent-light); + padding: 10px 16px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.75rem; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border); +} +.inline-diagram-content { + padding: 16px 20px; + display: flex; + justify-content: center; + align-items: center; +} +.inline-flow-svg { + width: 100%; + max-width: 500px; + height: auto; +} +.example-panel { + background: var(--bg-card); + border-radius: 12px; + overflow: hidden; + box-shadow: + 0 1px 3px #0d737714, + 0 8px 24px #0d73771f; + border: 2px solid var(--accent); +} +.example-panel.diagram-only { + background: var(--bg-diagram); +} +.example-header { + background: var(--bg-diagram); + padding: 16px 20px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); +} +.example-panel.diagram-only .example-header { + background: var(--bg-diagram); + border-bottom: 1px solid var(--border); +} +.example-title { + font-family: + JetBrains Mono, + monospace; + font-size: 0.9rem; + font-weight: 600; + color: var(--text-dark); + letter-spacing: 0.02em; +} +.example-badge { + background: var(--accent); + color: #fff; + padding: 5px 12px; + border-radius: 6px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.03em; +} +.example-tabs { + display: flex; + gap: 0; + background: var(--bg-card); + border-bottom: 1px solid var(--border); +} +.example-tab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 12px 16px; + background: transparent; + border: none; + cursor: pointer; + font-family: + JetBrains Mono, + monospace; + font-size: 0.75rem; + color: var(--text-mid); + transition: all 0.2s ease; + position: relative; +} +.example-tab:hover { + background: var(--accent-light); + color: var(--text-dark); +} +.example-tab.active { + background: var(--accent-light); + color: var(--accent); +} +.example-tab.active:after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + background: var(--accent); +} +.example-tab .tab-icon { + font-size: 0.9rem; +} +.example-tab .tab-label { + font-weight: 500; +} +.example-file-status.success { + background: #10b98126; + color: #059669; +} +.example-file-status.warning { + background: #b4530926; + color: #b45309; +} +.example-body { + padding: 20px 24px 24px; + min-height: 400px; + background: var(--bg-diagram); +} +.example-file-header { + font-family: + JetBrains Mono, + monospace; + font-size: 0.9rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} +.example-panel.diagram-only .example-body { + padding: 20px; + min-height: 350px; + display: flex; + flex-direction: column; +} +.diagram-title { + font-family: + JetBrains Mono, + monospace; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-dark); + text-align: center; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} +.diagram-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; +} +.diagram-container .flow-diagram-svg { + width: 100%; + height: auto; + max-height: 320px; +} +.example-section { + margin-bottom: 2px; +} +.example-section:last-child { + margin-bottom: 0; +} +.example-section-title { + font-family: + JetBrains Mono, + monospace; + font-size: 0.65rem; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.12em; + margin-top: 0; + margin-bottom: 4px; + display: flex; + align-items: center; + gap: 8px; +} +.example-section-title:after { + content: ''; + flex: 1; + height: 1px; + background: var(--border); +} +.example-code { + background: linear-gradient(135deg, #f5f7f7 0%, var(--bg-diagram) 100%); + border-radius: 8px; + padding: 16px 18px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.82rem; + line-height: 1.7; + color: var(--text-dark); + overflow-x: auto; + border: 1px solid var(--border); + box-shadow: inset 0 1px 3px #0000000a; +} +.example-code .comment { + color: var(--text-light); + font-style: italic; +} +.example-code .keyword { + color: var(--accent); + font-weight: 500; +} +.example-code .string { + color: #047857; +} +.example-code .number { + color: #b45309; +} +.example-code .error { + color: #dc2626; +} +.example-code .success { + color: #059669; +} +.example-file { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: var(--bg-card); + border-radius: 8px; + margin-bottom: 12px; + border: 1px solid var(--border); + box-shadow: 0 1px 3px #0000000a; +} +.example-file-icon { + font-size: 1.1rem; +} +.example-file-name { + font-family: + JetBrains Mono, + monospace; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-dark); +} +.example-file-status { + margin-left: auto; + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 4px; + font-family: + JetBrains Mono, + monospace; +} +.example-file-status.created { + background: #10b98126; + color: #059669; +} +.example-file-status.error { + background: #dc26261f; + color: #dc2626; +} +.example-output { + background: var(--bg-card); + border-radius: 8px; + padding: 16px 18px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.8rem; + border: 1px solid var(--border); +} +.example-output-line { + display: flex; + align-items: flex-start; + gap: 10px; + color: var(--text-mid); + transition: background 0.15s ease; + margin: 0 -8px; + padding: 2px 8px; + border-radius: 4px; +} +.example-output-line:hover { + background: var(--accent-light); +} +.example-output-line .icon { + flex-shrink: 0; + font-weight: 600; +} +.example-output-line.error { + color: #dc2626; +} +.example-output-line.error:hover { + background: #dc262614; +} +.example-output-line.success { + color: #059669; +} +.example-output-line.success:hover { + background: #05966914; +} +.example-output-line.warning { + color: #b45309; +} +.example-output-line.warning:hover { + background: #b4530914; +} +.example-diagram { + background: var(--bg-diagram); + border-radius: 8px; + padding: 20px; + margin-top: 16px; +} +.example-diagram svg { + width: 100%; + height: auto; +} +.example-diagram-container { + background: var(--bg-diagram); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--border); +} +.flow-diagram-svg { + width: 100%; + height: auto; + display: block; +} +@media (max-width: 1100px) { + .scrollytelling-container { + flex-direction: column; + } + .scrolly-sticky { + position: relative; + top: 0; + flex: none; + width: 100%; + order: -1; + margin-bottom: 30px; + } + .narrative-step { + min-height: auto; + padding: 30px 0; + opacity: 1; + } +} +@media (max-width: 768px) { + .wins-shortcomings { + grid-template-columns: 1fr; + } +} +.principles-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin: 40px 0; +} +.principle-card { + background: var(--bg-card); + border: 2px solid var(--border); + border-radius: 12px; + padding: 32px 28px; + transition: all 0.25s ease; + position: relative; + overflow: hidden; +} +.principle-card:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--accent); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.3s ease; +} +.principle-card:hover { + border-color: var(--accent); + transform: translateY(-6px); + box-shadow: 0 12px 40px #0d73771f; +} +.principle-card:hover:before { + transform: scaleX(1); +} +.principle-icon { + width: 56px; + height: 56px; + background: var(--accent-light); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + color: var(--accent); +} +.principle-icon svg { + width: 28px; + height: 28px; +} +.principle-title { + font-family: + JetBrains Mono, + monospace; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 12px; +} +.principle-desc { + font-family: 'Source Serif 4', Georgia, serif; + font-size: 1.05rem; + color: var(--text-mid); + line-height: 1.7; + margin: 0; +} +.principles-bottom { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + max-width: 820px; + margin: 0 auto; +} +.workflow-timeline { + background: var(--bg-diagram); + background-image: radial-gradient(circle, var(--grid-color) 1px, transparent 1px); + background-size: 20px 20px; + border-radius: 16px; + padding: 50px 40px; + margin: 40px 0; + border: 1px solid var(--border); +} +.workflow-header { + text-align: center; + margin-bottom: 40px; +} +.workflow-command { + display: inline-block; + background: var(--accent); + color: #fff; + padding: 14px 32px; + border-radius: 10px; + font-family: + JetBrains Mono, + monospace; + font-size: 1.2rem; + font-weight: 600; +} +.workflow-command-label { + font-family: + JetBrains Mono, + monospace; + font-size: 0.7rem; + color: var(--text-light); + text-transform: uppercase; + letter-spacing: 0.15em; + margin-bottom: 10px; +} +.timeline-phases { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + position: relative; +} +.timeline-phases:before { + content: ''; + position: absolute; + top: 25px; + left: 12%; + right: 12%; + height: 3px; + background: linear-gradient(90deg, var(--accent) 0%, var(--accent) 100%); + z-index: 0; +} +.timeline-phase { + position: relative; + z-index: 1; +} +.timeline-phase-header { + text-align: center; + margin-bottom: 16px; + transition: transform 0.2s ease; +} +.timeline-phase-header:hover { + transform: scale(1.05); +} +.timeline-phase-header:hover .timeline-phase-num { + box-shadow: 0 6px 20px #0d737766; +} +.timeline-phase-num { + display: inline-flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background: var(--accent); + color: #fff; + border-radius: 50%; + font-family: + JetBrains Mono, + monospace; + font-size: 1.2rem; + font-weight: 700; + margin-bottom: 10px; + box-shadow: 0 4px 15px #0d73774d; +} +.timeline-phase-title { + font-family: + JetBrains Mono, + monospace; + font-size: 1rem; + font-weight: 600; + color: var(--text-dark); +} +.timeline-phase-chevron { + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 8px; + color: var(--accent); + transition: transform 0.3s ease; +} +.timeline-phase-chevron.expanded { + transform: rotate(180deg); +} +.timeline-phase-card { + background: var(--bg-card); + border: 2px solid var(--border); + border-radius: 12px; + padding: 24px 20px; + min-height: 280px; + transition: all 0.25s ease; +} +.timeline-phase-card:hover { + border-color: var(--accent); + box-shadow: 0 8px 30px #0d73771a; +} +.timeline-agents { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 16px; +} +.timeline-agent { + background: var(--accent-light); + color: var(--accent); + padding: 4px 10px; + border-radius: 4px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.7rem; + font-weight: 500; +} +.timeline-phase-desc { + font-size: 0.95rem; + color: var(--text-mid); + line-height: 1.6; + margin: 0; +} +.timeline-phase-desc strong { + color: var(--text-dark); +} +.timeline-steps { + list-style: none; + padding: 0; + margin: 12px 0 0; +} +.timeline-steps li { + font-size: 0.9rem; + color: var(--text-mid); + padding: 4px 0 4px 18px; + position: relative; +} +.timeline-steps li:before { + content: '→'; + position: absolute; + left: 0; + color: var(--accent); +} +.results-section { + margin: 40px 0; +} +.results-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 40px; +} +.stat-card { + background: linear-gradient(135deg, var(--accent) 0%, #0a5d60 100%); + border-radius: 16px; + padding: 36px 28px; + text-align: center; + color: #fff; + position: relative; + overflow: hidden; +} +.stat-card:before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 60%); +} +.stat-number { + font-family: + JetBrains Mono, + monospace; + font-size: 3rem; + font-weight: 700; + margin-bottom: 8px; + position: relative; +} +.stat-label { + font-family: 'Source Serif 4', Georgia, serif; + font-size: 1.1rem; + opacity: 0.9; + position: relative; +} +.stat-detail { + font-size: 0.85rem; + opacity: 0.75; + margin-top: 8px; + position: relative; +} +.results-description { + background: var(--bg-diagram); + border-radius: 12px; + padding: 28px 32px; + border-left: 4px solid var(--accent); +} +.results-description p { + margin: 0; + font-size: 1.1rem; +} +.terminal-container { + background: #1a1a2e; + border-radius: 12px; + overflow: hidden; + margin: 30px 0; + box-shadow: 0 8px 30px #0003; +} +.terminal-header { + background: #16162a; + padding: 12px 16px; + display: flex; + align-items: center; + gap: 8px; +} +.terminal-dot { + width: 12px; + height: 12px; + border-radius: 50%; +} +.terminal-dot.red { + background: #ff5f57; +} +.terminal-dot.yellow { + background: #febc2e; +} +.terminal-dot.green { + background: #28c840; +} +.terminal-title { + margin-left: 12px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.8rem; + color: #666; +} +.terminal-body { + padding: 24px 28px; +} +.terminal-body pre { + background: transparent; + border: none; + padding: 0; + margin: 0; + color: #e8f0f0; +} +.terminal-body code { + background: transparent; + color: inherit; + font-size: 1rem; +} +.terminal-comment { + color: #6b7280; +} +.terminal-command { + color: #10b981; +} +.next-cards { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 28px; + margin: 40px 0; +} +.next-card { + background: var(--bg-card); + border: 2px solid var(--border); + border-radius: 12px; + padding: 32px 28px; + position: relative; + transition: all 0.25s ease; +} +.next-card:hover { + border-color: var(--accent); + transform: translateY(-4px); + box-shadow: 0 12px 40px #0d73771a; +} +.next-card-badge { + display: inline-block; + background: var(--accent-light); + color: var(--accent); + padding: 6px 14px; + border-radius: 20px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 16px; +} +.next-card-title { + font-family: + JetBrains Mono, + monospace; + font-size: 1.2rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 14px; +} +.next-card-desc { + font-size: 1.05rem; + color: var(--text-mid); + line-height: 1.7; + margin: 0; +} +.agent-flow-container { + background: var(--bg-diagram); + background-image: radial-gradient(circle, var(--grid-color) 1px, transparent 1px); + background-size: 20px 20px; + border-radius: 12px; + padding: 60px 40px 50px; + margin: 50px 0; + position: relative; + border: 1px solid var(--border); + box-shadow: inset 0 2px 8px #00000008; + overflow-x: auto; +} +.agent-flow-container:before { + content: 'AGENT FLOW'; + position: absolute; + top: 18px; + left: 24px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.2em; + color: var(--text-light); + text-transform: uppercase; +} +.agent-flow-svg { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; +} +.agent-flow-svg .network-node { + fill: var(--bg-card); + stroke: var(--accent); + stroke-width: 2; +} +.agent-flow-svg .network-line { + stroke: var(--text-mid); + stroke-width: 2; + stroke-dasharray: 6 4; + fill: none; +} +.agent-flow-svg .network-line.thick { + stroke-width: 2.5; +} +.agent-flow-svg .network-label { + font-family: + JetBrains Mono, + monospace; + font-size: 13px; + fill: var(--text-mid); + text-anchor: middle; +} +.agent-flow-svg .group-label { + font-family: + JetBrains Mono, + monospace; + font-size: 14px; + font-weight: 600; + fill: var(--accent); + text-anchor: middle; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.agent-flow-svg .flow-arrow { + fill: var(--accent); +} +.agent-flow-svg .svg-icon { + color: var(--text-mid); + pointer-events: none; +} +.agent-flow-svg .loop-indicator { + fill: #0d737708; + stroke: var(--accent); + stroke-width: 2.5; + stroke-dasharray: 12 6; + opacity: 0.7; + animation: rotateLoop 6s linear infinite; +} +@keyframes rotateLoop { + 0% { + stroke-dashoffset: 0; + } + to { + stroke-dashoffset: -72; + } +} +.agent-flow-svg .loop-label { + font-family: + JetBrains Mono, + monospace; + font-size: 13px; + font-weight: 600; + font-style: italic; + fill: #1a202c; + text-anchor: middle; + letter-spacing: 0.03em; +} +.stage-diagram { + padding: 30px 20px 25px; + margin: 25px auto; + max-width: 100%; + overflow-x: auto; +} +.stage-diagram svg { + display: block; + margin: 0 auto; + width: 100%; + height: auto; +} +.stages-diagram-container { + background: var(--bg-diagram); + border-radius: 12px; + padding: 20px; + margin: 30px 0; + border: 1px solid var(--border); + overflow-x: auto; +} +.stages-diagram-svg { + display: block; + margin: 0 auto; + width: 100%; + max-width: 1100px; + height: auto; + min-height: 400px; +} +.stages-diagram-svg .network-node { + fill: var(--bg-card); + stroke: var(--accent); + stroke-width: 2; +} +.stages-diagram-svg .network-line { + stroke: var(--text-mid); + stroke-width: 1.5; + stroke-dasharray: 5 3; + fill: none; +} +.stages-diagram-svg .network-label { + font-family: + JetBrains Mono, + monospace; + font-size: 11px; + fill: var(--text-mid); + text-anchor: middle; +} +.stages-diagram-svg .svg-icon { + color: var(--text-mid); + pointer-events: none; +} +.stages-diagram-svg .loop-indicator { + fill: #0d737708; + stroke: var(--accent); + stroke-width: 2.5; + stroke-dasharray: 12 6; + opacity: 0.6; + animation: rotateLoop 6s linear infinite; +} +@media (max-width: 1200px) { + .article-wrapper { + padding: 60px; + } + h1 { + font-size: 2.4rem; + } + .principles-grid { + grid-template-columns: repeat(2, 1fr); + } + .principles-bottom { + grid-template-columns: 1fr; + max-width: 100%; + } + .timeline-phases { + grid-template-columns: repeat(2, 1fr); + } + .timeline-phases:before { + display: none; + } +} +@media (max-width: 768px) { + .article-wrapper { + padding: 30px 25px; + } + h1 { + font-size: 1.8rem; + } + .iteration-cards { + flex-direction: column; + } + .iteration-connector { + display: none; + } + .principles-grid, + .timeline-phases, + .results-stats, + .next-cards { + grid-template-columns: 1fr; + } +} +.step-diagram.clickable { + cursor: zoom-in; + position: relative; + transition: all 0.2s ease; +} +.step-diagram.clickable:hover { + border-color: var(--accent); + box-shadow: 0 4px 20px #0d737726; +} +.step-diagram.clickable:hover .expand-hint { + opacity: 1; +} +.expand-hint { + position: absolute; + bottom: 12px; + right: 16px; + background: var(--accent); + color: #fff; + padding: 6px 12px; + border-radius: 6px; + font-family: + JetBrains Mono, + monospace; + font-size: 0.7rem; + font-weight: 500; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; +} +.diagram-modal-overlay { + position: fixed; + inset: 0; + background: #000000d9; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + padding: 40px; + cursor: zoom-out; +} +.diagram-modal-content { + background: var(--bg-diagram); + background-image: radial-gradient(circle, var(--grid-color) 1px, transparent 1px); + background-size: 20px 20px; + border-radius: 16px; + padding: 32px 40px 40px; + max-width: 95vw; + max-height: 90vh; + overflow: auto; + position: relative; + cursor: default; + box-shadow: 0 20px 60px #0006; + border: 2px solid var(--accent); +} +.diagram-modal-close { + position: absolute; + top: 16px; + right: 20px; + background: var(--accent); + color: #fff; + border: none; + width: 36px; + height: 36px; + border-radius: 50%; + font-size: 24px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} +.diagram-modal-close:hover { + background: #0a5d60; + transform: scale(1.1); +} +.diagram-modal-title { + font-family: + JetBrains Mono, + monospace; + font-size: 1.2rem; + font-weight: 600; + color: var(--text-dark); + margin-bottom: 24px; + padding-right: 50px; +} +.diagram-modal-diagram { + display: flex; + justify-content: center; + align-items: center; + min-width: 800px; +} +.diagram-modal-diagram svg { + width: 100%; + height: auto; + max-height: 70vh; +} +@media (max-width: 900px) { + .diagram-modal-overlay { + padding: 20px; + } + .diagram-modal-content { + padding: 24px; + } + .diagram-modal-diagram { + min-width: auto; + } +} diff --git a/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-DC8RyDPP.js b/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-DC8RyDPP.js index d85938477..018eb690f 100644 --- a/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-DC8RyDPP.js +++ b/app/public/assets/posts/encode-policy-multi-agent-ai/assets/index-DC8RyDPP.js @@ -1,12 +1,16321 @@ -(function(){const l=document.createElement("link").relList;if(l&&l.supports&&l.supports("modulepreload"))return;for(const f of document.querySelectorAll('link[rel="modulepreload"]'))r(f);new MutationObserver(f=>{for(const m of f)if(m.type==="childList")for(const d of m.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&r(d)}).observe(document,{childList:!0,subtree:!0});function o(f){const m={};return f.integrity&&(m.integrity=f.integrity),f.referrerPolicy&&(m.referrerPolicy=f.referrerPolicy),f.crossOrigin==="use-credentials"?m.credentials="include":f.crossOrigin==="anonymous"?m.credentials="omit":m.credentials="same-origin",m}function r(f){if(f.ep)return;f.ep=!0;const m=o(f);fetch(f.href,m)}})();function lv(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Uu={exports:{}},sl={};var gp;function sv(){if(gp)return sl;gp=1;var i=Symbol.for("react.transitional.element"),l=Symbol.for("react.fragment");function o(r,f,m){var d=null;if(m!==void 0&&(d=""+m),f.key!==void 0&&(d=""+f.key),"key"in f){m={};for(var p in f)p!=="key"&&(m[p]=f[p])}else m=f;return f=m.ref,{$$typeof:i,type:r,key:d,ref:f!==void 0?f:null,props:m}}return sl.Fragment=l,sl.jsx=o,sl.jsxs=o,sl}var xp;function rv(){return xp||(xp=1,Uu.exports=sv()),Uu.exports}var c=rv(),Hu={exports:{}},at={};var vp;function ov(){if(vp)return at;vp=1;var i=Symbol.for("react.transitional.element"),l=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),f=Symbol.for("react.profiler"),m=Symbol.for("react.consumer"),d=Symbol.for("react.context"),p=Symbol.for("react.forward_ref"),y=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),v=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),T=Symbol.iterator;function w(A){return A===null||typeof A!="object"?null:(A=T&&A[T]||A["@@iterator"],typeof A=="function"?A:null)}var N={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},H=Object.assign,G={};function U(A,k,X){this.props=A,this.context=k,this.refs=G,this.updater=X||N}U.prototype.isReactComponent={},U.prototype.setState=function(A,k){if(typeof A!="object"&&typeof A!="function"&&A!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,A,k,"setState")},U.prototype.forceUpdate=function(A){this.updater.enqueueForceUpdate(this,A,"forceUpdate")};function q(){}q.prototype=U.prototype;function V(A,k,X){this.props=A,this.context=k,this.refs=G,this.updater=X||N}var Z=V.prototype=new q;Z.constructor=V,H(Z,U.prototype),Z.isPureReactComponent=!0;var Q=Array.isArray;function nt(){}var F={H:null,A:null,T:null,S:null},K=Object.prototype.hasOwnProperty;function it(A,k,X){var $=X.ref;return{$$typeof:i,type:A,key:k,ref:$!==void 0?$:null,props:X}}function yt(A,k){return it(A.type,k,A.props)}function gt(A){return typeof A=="object"&&A!==null&&A.$$typeof===i}function Nt(A){var k={"=":"=0",":":"=2"};return"$"+A.replace(/[=:]/g,function(X){return k[X]})}var Jt=/\/+/g;function Ht(A,k){return typeof A=="object"&&A!==null&&A.key!=null?Nt(""+A.key):k.toString(36)}function Qt(A){switch(A.status){case"fulfilled":return A.value;case"rejected":throw A.reason;default:switch(typeof A.status=="string"?A.then(nt,nt):(A.status="pending",A.then(function(k){A.status==="pending"&&(A.status="fulfilled",A.value=k)},function(k){A.status==="pending"&&(A.status="rejected",A.reason=k)})),A.status){case"fulfilled":return A.value;case"rejected":throw A.reason}}throw A}function z(A,k,X,$,lt){var ut=typeof A;(ut==="undefined"||ut==="boolean")&&(A=null);var St=!1;if(A===null)St=!0;else switch(ut){case"bigint":case"string":case"number":St=!0;break;case"object":switch(A.$$typeof){case i:case l:St=!0;break;case v:return St=A._init,z(St(A._payload),k,X,$,lt)}}if(St)return lt=lt(A),St=$===""?"."+Ht(A,0):$,Q(lt)?(X="",St!=null&&(X=St.replace(Jt,"$&/")+"/"),z(lt,k,X,"",function(ha){return ha})):lt!=null&&(gt(lt)&&(lt=yt(lt,X+(lt.key==null||A&&A.key===lt.key?"":(""+lt.key).replace(Jt,"$&/")+"/")+St)),k.push(lt)),1;St=0;var se=$===""?".":$+":";if(Q(A))for(var Vt=0;Vt>>1,dt=z[ot];if(0>>1;otf(X,P))$f(lt,X)?(z[ot]=lt,z[$]=P,ot=$):(z[ot]=X,z[k]=P,ot=k);else if($f(lt,P))z[ot]=lt,z[$]=P,ot=$;else break t}}return B}function f(z,B){var P=z.sortIndex-B.sortIndex;return P!==0?P:z.id-B.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var m=performance;i.unstable_now=function(){return m.now()}}else{var d=Date,p=d.now();i.unstable_now=function(){return d.now()-p}}var y=[],g=[],v=1,b=null,T=3,w=!1,N=!1,H=!1,G=!1,U=typeof setTimeout=="function"?setTimeout:null,q=typeof clearTimeout=="function"?clearTimeout:null,V=typeof setImmediate<"u"?setImmediate:null;function Z(z){for(var B=o(g);B!==null;){if(B.callback===null)r(g);else if(B.startTime<=z)r(g),B.sortIndex=B.expirationTime,l(y,B);else break;B=o(g)}}function Q(z){if(H=!1,Z(z),!N)if(o(y)!==null)N=!0,nt||(nt=!0,Nt());else{var B=o(g);B!==null&&Qt(Q,B.startTime-z)}}var nt=!1,F=-1,K=5,it=-1;function yt(){return G?!0:!(i.unstable_now()-itz&&yt());){var ot=b.callback;if(typeof ot=="function"){b.callback=null,T=b.priorityLevel;var dt=ot(b.expirationTime<=z);if(z=i.unstable_now(),typeof dt=="function"){b.callback=dt,Z(z),B=!0;break e}b===o(y)&&r(y),Z(z)}else r(y);b=o(y)}if(b!==null)B=!0;else{var A=o(g);A!==null&&Qt(Q,A.startTime-z),B=!1}}break t}finally{b=null,T=P,w=!1}B=void 0}}finally{B?Nt():nt=!1}}}var Nt;if(typeof V=="function")Nt=function(){V(gt)};else if(typeof MessageChannel<"u"){var Jt=new MessageChannel,Ht=Jt.port2;Jt.port1.onmessage=gt,Nt=function(){Ht.postMessage(null)}}else Nt=function(){U(gt,0)};function Qt(z,B){F=U(function(){z(i.unstable_now())},B)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(z){z.callback=null},i.unstable_forceFrameRate=function(z){0>z||125ot?(z.sortIndex=P,l(g,z),o(y)===null&&z===o(g)&&(H?(q(F),F=-1):H=!0,Qt(Q,P-ot))):(z.sortIndex=dt,l(y,z),N||w||(N=!0,nt||(nt=!0,Nt()))),z},i.unstable_shouldYield=yt,i.unstable_wrapCallback=function(z){var B=T;return function(){var P=T;T=B;try{return z.apply(this,arguments)}finally{T=P}}}})(Gu)),Gu}var jp;function cv(){return jp||(jp=1,qu.exports=uv()),qu.exports}var Xu={exports:{}},ae={};var Tp;function fv(){if(Tp)return ae;Tp=1;var i=kc();function l(y){var g="https://react.dev/errors/"+y;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(l){console.error(l)}}return i(),Xu.exports=fv(),Xu.exports}var Cp;function hv(){if(Cp)return rl;Cp=1;var i=cv(),l=kc(),o=dv();function r(t){var e="https://react.dev/errors/"+t;if(1dt||(t.current=ot[dt],ot[dt]=null,dt--)}function X(t,e){dt++,ot[dt]=t.current,t.current=e}var $=A(null),lt=A(null),ut=A(null),St=A(null);function se(t,e){switch(X(ut,e),X(lt,t),X($,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?Hm(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=Hm(e),t=Ym(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}k($),X($,t)}function Vt(){k($),k(lt),k(ut)}function ha(t){t.memoizedState!==null&&X(St,t);var e=$.current,n=Ym(e,t.type);e!==n&&(X(lt,t),X($,n))}function Dl(t){lt.current===t&&(k($),k(lt)),St.current===t&&(k(St),nl._currentValue=P)}var br,yf;function Fn(t){if(br===void 0)try{throw Error()}catch(n){var e=n.stack.trim().match(/\n( *(at )?)/);br=e&&e[1]||"",yf=-1)":-1s||S[a]!==E[s]){var _=` -`+S[a].replace(" at new "," at ");return t.displayName&&_.includes("")&&(_=_.replace("",t.displayName)),_}while(1<=a&&0<=s);break}}}finally{Sr=!1,Error.prepareStackTrace=n}return(n=t?t.displayName||t.name:"")?Fn(n):""}function kg(t,e){switch(t.tag){case 26:case 27:case 5:return Fn(t.type);case 16:return Fn("Lazy");case 13:return t.child!==e&&e!==null?Fn("Suspense Fallback"):Fn("Suspense");case 19:return Fn("SuspenseList");case 0:case 15:return jr(t.type,!1);case 11:return jr(t.type.render,!1);case 1:return jr(t.type,!0);case 31:return Fn("Activity");default:return""}}function gf(t){try{var e="",n=null;do e+=kg(t,n),n=t,t=t.return;while(t);return e}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}var Tr=Object.prototype.hasOwnProperty,Ar=i.unstable_scheduleCallback,Cr=i.unstable_cancelCallback,Vg=i.unstable_shouldYield,Bg=i.unstable_requestPaint,ge=i.unstable_now,Ug=i.unstable_getCurrentPriorityLevel,xf=i.unstable_ImmediatePriority,vf=i.unstable_UserBlockingPriority,wl=i.unstable_NormalPriority,Hg=i.unstable_LowPriority,bf=i.unstable_IdlePriority,Yg=i.log,qg=i.unstable_setDisableYieldValue,ma=null,xe=null;function xn(t){if(typeof Yg=="function"&&qg(t),xe&&typeof xe.setStrictMode=="function")try{xe.setStrictMode(ma,t)}catch{}}var ve=Math.clz32?Math.clz32:Zg,Gg=Math.log,Xg=Math.LN2;function Zg(t){return t>>>=0,t===0?32:31-(Gg(t)/Xg|0)|0}var Ll=256,Nl=262144,_l=4194304;function Pn(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function zl(t,e,n){var a=t.pendingLanes;if(a===0)return 0;var s=0,u=t.suspendedLanes,h=t.pingedLanes;t=t.warmLanes;var x=a&134217727;return x!==0?(a=x&~u,a!==0?s=Pn(a):(h&=x,h!==0?s=Pn(h):n||(n=x&~t,n!==0&&(s=Pn(n))))):(x=a&~u,x!==0?s=Pn(x):h!==0?s=Pn(h):n||(n=a&~t,n!==0&&(s=Pn(n)))),s===0?0:e!==0&&e!==s&&(e&u)===0&&(u=s&-s,n=e&-e,u>=n||u===32&&(n&4194048)!==0)?e:s}function pa(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Qg(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Sf(){var t=_l;return _l<<=1,(_l&62914560)===0&&(_l=4194304),t}function Mr(t){for(var e=[],n=0;31>n;n++)e.push(t);return e}function ya(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Kg(t,e,n,a,s,u){var h=t.pendingLanes;t.pendingLanes=n,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=n,t.entangledLanes&=n,t.errorRecoveryDisabledLanes&=n,t.shellSuspendCounter=0;var x=t.entanglements,S=t.expirationTimes,E=t.hiddenUpdates;for(n=h&~n;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Ig=/[\n"\\]/g;function De(t){return t.replace(Ig,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function _r(t,e,n,a,s,u,h,x){t.name="",h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"?t.type=h:t.removeAttribute("type"),e!=null?h==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+Ee(e)):t.value!==""+Ee(e)&&(t.value=""+Ee(e)):h!=="submit"&&h!=="reset"||t.removeAttribute("value"),e!=null?zr(t,h,Ee(e)):n!=null?zr(t,h,Ee(n)):a!=null&&t.removeAttribute("value"),s==null&&u!=null&&(t.defaultChecked=!!u),s!=null&&(t.checked=s&&typeof s!="function"&&typeof s!="symbol"),x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"?t.name=""+Ee(x):t.removeAttribute("name")}function Rf(t,e,n,a,s,u,h,x){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(t.type=u),e!=null||n!=null){if(!(u!=="submit"&&u!=="reset"||e!=null)){Nr(t);return}n=n!=null?""+Ee(n):"",e=e!=null?""+Ee(e):n,x||e===t.value||(t.value=e),t.defaultValue=e}a=a??s,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=x?t.checked:!!a,t.defaultChecked=!!a,h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"&&(t.name=h),Nr(t)}function zr(t,e,n){e==="number"&&kl(t.ownerDocument)===t||t.defaultValue===""+n||(t.defaultValue=""+n)}function Ci(t,e,n,a){if(t=t.options,e){e={};for(var s=0;s"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Br=!1;if(Ie)try{var ba={};Object.defineProperty(ba,"passive",{get:function(){Br=!0}}),window.addEventListener("test",ba,ba),window.removeEventListener("test",ba,ba)}catch{Br=!1}var bn=null,Ur=null,Bl=null;function Yf(){if(Bl)return Bl;var t,e=Ur,n=e.length,a,s="value"in bn?bn.value:bn.textContent,u=s.length;for(t=0;t=Ta),Kf=" ",Wf=!1;function Jf(t,e){switch(t){case"keyup":return E1.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ff(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var wi=!1;function w1(t,e){switch(t){case"compositionend":return Ff(e);case"keypress":return e.which!==32?null:(Wf=!0,Kf);case"textInput":return t=e.data,t===Kf&&Wf?null:t;default:return null}}function L1(t,e){if(wi)return t==="compositionend"||!Xr&&Jf(t,e)?(t=Yf(),Bl=Ur=bn=null,wi=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:n,offset:e-t};t=a}t:{for(;n;){if(n.nextSibling){n=n.nextSibling;break t}n=n.parentNode}n=void 0}n=ad(n)}}function sd(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?sd(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function rd(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=kl(t.document);e instanceof t.HTMLIFrameElement;){try{var n=typeof e.contentWindow.location.href=="string"}catch{n=!1}if(n)t=e.contentWindow;else break;e=kl(t.document)}return e}function Kr(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var B1=Ie&&"documentMode"in document&&11>=document.documentMode,Li=null,Wr=null,Ea=null,Jr=!1;function od(t,e,n){var a=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Jr||Li==null||Li!==kl(a)||(a=Li,"selectionStart"in a&&Kr(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Ea&&Ma(Ea,a)||(Ea=a,a=Ns(Wr,"onSelect"),0>=h,s-=h,Ze=1<<32-ve(e)+s|n<rt?(mt=I,I=null):mt=I.sibling;var vt=D(C,I,M[rt],R);if(vt===null){I===null&&(I=mt);break}t&&I&&vt.alternate===null&&e(C,I),j=u(vt,j,rt),xt===null?tt=vt:xt.sibling=vt,xt=vt,I=mt}if(rt===M.length)return n(C,I),pt&&en(C,rt),tt;if(I===null){for(;rtrt?(mt=I,I=null):mt=I.sibling;var qn=D(C,I,vt.value,R);if(qn===null){I===null&&(I=mt);break}t&&I&&qn.alternate===null&&e(C,I),j=u(qn,j,rt),xt===null?tt=qn:xt.sibling=qn,xt=qn,I=mt}if(vt.done)return n(C,I),pt&&en(C,rt),tt;if(I===null){for(;!vt.done;rt++,vt=M.next())vt=O(C,vt.value,R),vt!==null&&(j=u(vt,j,rt),xt===null?tt=vt:xt.sibling=vt,xt=vt);return pt&&en(C,rt),tt}for(I=a(I);!vt.done;rt++,vt=M.next())vt=L(I,C,rt,vt.value,R),vt!==null&&(t&&vt.alternate!==null&&I.delete(vt.key===null?rt:vt.key),j=u(vt,j,rt),xt===null?tt=vt:xt.sibling=vt,xt=vt);return t&&I.forEach(function(av){return e(C,av)}),pt&&en(C,rt),tt}function Mt(C,j,M,R){if(typeof M=="object"&&M!==null&&M.type===H&&M.key===null&&(M=M.props.children),typeof M=="object"&&M!==null){switch(M.$$typeof){case w:t:{for(var tt=M.key;j!==null;){if(j.key===tt){if(tt=M.type,tt===H){if(j.tag===7){n(C,j.sibling),R=s(j,M.props.children),R.return=C,C=R;break t}}else if(j.elementType===tt||typeof tt=="object"&&tt!==null&&tt.$$typeof===K&&oi(tt)===j.type){n(C,j.sibling),R=s(j,M.props),za(R,M),R.return=C,C=R;break t}n(C,j);break}else e(C,j);j=j.sibling}M.type===H?(R=ii(M.props.children,C.mode,R,M.key),R.return=C,C=R):(R=Wl(M.type,M.key,M.props,null,C.mode,R),za(R,M),R.return=C,C=R)}return h(C);case N:t:{for(tt=M.key;j!==null;){if(j.key===tt)if(j.tag===4&&j.stateNode.containerInfo===M.containerInfo&&j.stateNode.implementation===M.implementation){n(C,j.sibling),R=s(j,M.children||[]),R.return=C,C=R;break t}else{n(C,j);break}else e(C,j);j=j.sibling}R=no(M,C.mode,R),R.return=C,C=R}return h(C);case K:return M=oi(M),Mt(C,j,M,R)}if(Qt(M))return W(C,j,M,R);if(Nt(M)){if(tt=Nt(M),typeof tt!="function")throw Error(r(150));return M=tt.call(M),et(C,j,M,R)}if(typeof M.then=="function")return Mt(C,j,es(M),R);if(M.$$typeof===V)return Mt(C,j,Pl(C,M),R);ns(C,M)}return typeof M=="string"&&M!==""||typeof M=="number"||typeof M=="bigint"?(M=""+M,j!==null&&j.tag===6?(n(C,j.sibling),R=s(j,M),R.return=C,C=R):(n(C,j),R=eo(M,C.mode,R),R.return=C,C=R),h(C)):n(C,j)}return function(C,j,M,R){try{_a=0;var tt=Mt(C,j,M,R);return Yi=null,tt}catch(I){if(I===Hi||I===Il)throw I;var xt=Se(29,I,null,C.mode);return xt.lanes=R,xt.return=C,xt}}}var ci=Nd(!0),_d=Nd(!1),Cn=!1;function po(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function yo(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Mn(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function En(t,e,n){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(bt&2)!==0){var s=a.pending;return s===null?e.next=e:(e.next=s.next,s.next=e),a.pending=e,e=Kl(t),pd(t,null,n),e}return Ql(t,a,e,n),Kl(t)}function Ra(t,e,n){if(e=e.updateQueue,e!==null&&(e=e.shared,(n&4194048)!==0)){var a=e.lanes;a&=t.pendingLanes,n|=a,e.lanes=n,Tf(t,n)}}function go(t,e){var n=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,n===a)){var s=null,u=null;if(n=n.firstBaseUpdate,n!==null){do{var h={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};u===null?s=u=h:u=u.next=h,n=n.next}while(n!==null);u===null?s=u=e:u=u.next=e}else s=u=e;n={baseState:a.baseState,firstBaseUpdate:s,lastBaseUpdate:u,shared:a.shared,callbacks:a.callbacks},t.updateQueue=n;return}t=n.lastBaseUpdate,t===null?n.firstBaseUpdate=e:t.next=e,n.lastBaseUpdate=e}var xo=!1;function Oa(){if(xo){var t=Ui;if(t!==null)throw t}}function ka(t,e,n,a){xo=!1;var s=t.updateQueue;Cn=!1;var u=s.firstBaseUpdate,h=s.lastBaseUpdate,x=s.shared.pending;if(x!==null){s.shared.pending=null;var S=x,E=S.next;S.next=null,h===null?u=E:h.next=E,h=S;var _=t.alternate;_!==null&&(_=_.updateQueue,x=_.lastBaseUpdate,x!==h&&(x===null?_.firstBaseUpdate=E:x.next=E,_.lastBaseUpdate=S))}if(u!==null){var O=s.baseState;h=0,_=E=S=null,x=u;do{var D=x.lane&-536870913,L=D!==x.lane;if(L?(ht&D)===D:(a&D)===D){D!==0&&D===Bi&&(xo=!0),_!==null&&(_=_.next={lane:0,tag:x.tag,payload:x.payload,callback:null,next:null});t:{var W=t,et=x;D=e;var Mt=n;switch(et.tag){case 1:if(W=et.payload,typeof W=="function"){O=W.call(Mt,O,D);break t}O=W;break t;case 3:W.flags=W.flags&-65537|128;case 0:if(W=et.payload,D=typeof W=="function"?W.call(Mt,O,D):W,D==null)break t;O=b({},O,D);break t;case 2:Cn=!0}}D=x.callback,D!==null&&(t.flags|=64,L&&(t.flags|=8192),L=s.callbacks,L===null?s.callbacks=[D]:L.push(D))}else L={lane:D,tag:x.tag,payload:x.payload,callback:x.callback,next:null},_===null?(E=_=L,S=O):_=_.next=L,h|=D;if(x=x.next,x===null){if(x=s.shared.pending,x===null)break;L=x,x=L.next,L.next=null,s.lastBaseUpdate=L,s.shared.pending=null}}while(!0);_===null&&(S=O),s.baseState=S,s.firstBaseUpdate=E,s.lastBaseUpdate=_,u===null&&(s.shared.lanes=0),_n|=h,t.lanes=h,t.memoizedState=O}}function zd(t,e){if(typeof t!="function")throw Error(r(191,t));t.call(e)}function Rd(t,e){var n=t.callbacks;if(n!==null)for(t.callbacks=null,t=0;tu?u:8;var h=z.T,x={};z.T=x,Vo(t,!1,e,n);try{var S=s(),E=z.S;if(E!==null&&E(x,S),S!==null&&typeof S=="object"&&typeof S.then=="function"){var _=K1(S,a);Ua(t,e,_,Me(t))}else Ua(t,e,a,Me(t))}catch(O){Ua(t,e,{then:function(){},status:"rejected",reason:O},Me())}finally{B.p=u,h!==null&&x.types!==null&&(h.types=x.types),z.T=h}}function I1(){}function Oo(t,e,n,a){if(t.tag!==5)throw Error(r(476));var s=dh(t).queue;fh(t,s,e,P,n===null?I1:function(){return hh(t),n(a)})}function dh(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:P,baseState:P,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:sn,lastRenderedState:P},next:null};var n={};return e.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:sn,lastRenderedState:n},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function hh(t){var e=dh(t);e.next===null&&(e=t.alternate.memoizedState),Ua(t,e.next.queue,{},Me())}function ko(){return te(nl)}function mh(){return Ut().memoizedState}function ph(){return Ut().memoizedState}function tx(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var n=Me();t=Mn(n);var a=En(e,t,n);a!==null&&(ye(a,e,n),Ra(a,e,n)),e={cache:co()},t.payload=e;return}e=e.return}}function ex(t,e,n){var a=Me();n={lane:a,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},ds(t)?gh(e,n):(n=Ir(t,e,n,a),n!==null&&(ye(n,t,a),xh(n,e,a)))}function yh(t,e,n){var a=Me();Ua(t,e,n,a)}function Ua(t,e,n,a){var s={lane:a,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(ds(t))gh(e,s);else{var u=t.alternate;if(t.lanes===0&&(u===null||u.lanes===0)&&(u=e.lastRenderedReducer,u!==null))try{var h=e.lastRenderedState,x=u(h,n);if(s.hasEagerState=!0,s.eagerState=x,be(x,h))return Ql(t,e,s,0),Et===null&&Zl(),!1}catch{}if(n=Ir(t,e,s,a),n!==null)return ye(n,t,a),xh(n,e,a),!0}return!1}function Vo(t,e,n,a){if(a={lane:2,revertLane:pu(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},ds(t)){if(e)throw Error(r(479))}else e=Ir(t,n,a,2),e!==null&&ye(e,t,2)}function ds(t){var e=t.alternate;return t===st||e!==null&&e===st}function gh(t,e){Gi=ls=!0;var n=t.pending;n===null?e.next=e:(e.next=n.next,n.next=e),t.pending=e}function xh(t,e,n){if((n&4194048)!==0){var a=e.lanes;a&=t.pendingLanes,n|=a,e.lanes=n,Tf(t,n)}}var Ha={readContext:te,use:os,useCallback:Rt,useContext:Rt,useEffect:Rt,useImperativeHandle:Rt,useLayoutEffect:Rt,useInsertionEffect:Rt,useMemo:Rt,useReducer:Rt,useRef:Rt,useState:Rt,useDebugValue:Rt,useDeferredValue:Rt,useTransition:Rt,useSyncExternalStore:Rt,useId:Rt,useHostTransitionStatus:Rt,useFormState:Rt,useActionState:Rt,useOptimistic:Rt,useMemoCache:Rt,useCacheRefresh:Rt};Ha.useEffectEvent=Rt;var vh={readContext:te,use:os,useCallback:function(t,e){return re().memoizedState=[t,e===void 0?null:e],t},useContext:te,useEffect:nh,useImperativeHandle:function(t,e,n){n=n!=null?n.concat([t]):null,cs(4194308,4,sh.bind(null,e,t),n)},useLayoutEffect:function(t,e){return cs(4194308,4,t,e)},useInsertionEffect:function(t,e){cs(4,2,t,e)},useMemo:function(t,e){var n=re();e=e===void 0?null:e;var a=t();if(fi){xn(!0);try{t()}finally{xn(!1)}}return n.memoizedState=[a,e],a},useReducer:function(t,e,n){var a=re();if(n!==void 0){var s=n(e);if(fi){xn(!0);try{n(e)}finally{xn(!1)}}}else s=e;return a.memoizedState=a.baseState=s,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:s},a.queue=t,t=t.dispatch=ex.bind(null,st,t),[a.memoizedState,t]},useRef:function(t){var e=re();return t={current:t},e.memoizedState=t},useState:function(t){t=Lo(t);var e=t.queue,n=yh.bind(null,st,e);return e.dispatch=n,[t.memoizedState,n]},useDebugValue:zo,useDeferredValue:function(t,e){var n=re();return Ro(n,t,e)},useTransition:function(){var t=Lo(!1);return t=fh.bind(null,st,t.queue,!0,!1),re().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,n){var a=st,s=re();if(pt){if(n===void 0)throw Error(r(407));n=n()}else{if(n=e(),Et===null)throw Error(r(349));(ht&127)!==0||Hd(a,e,n)}s.memoizedState=n;var u={value:n,getSnapshot:e};return s.queue=u,nh(qd.bind(null,a,u,t),[t]),a.flags|=2048,Zi(9,{destroy:void 0},Yd.bind(null,a,u,n,e),null),n},useId:function(){var t=re(),e=Et.identifierPrefix;if(pt){var n=Qe,a=Ze;n=(a&~(1<<32-ve(a)-1)).toString(32)+n,e="_"+e+"R_"+n,n=ss++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof a.is=="string"?h.createElement("select",{is:a.is}):h.createElement("select"),a.multiple?u.multiple=!0:a.size&&(u.size=a.size);break;default:u=typeof a.is=="string"?h.createElement(s,{is:a.is}):h.createElement(s)}}u[$t]=e,u[ce]=a;t:for(h=e.child;h!==null;){if(h.tag===5||h.tag===6)u.appendChild(h.stateNode);else if(h.tag!==4&&h.tag!==27&&h.child!==null){h.child.return=h,h=h.child;continue}if(h===e)break t;for(;h.sibling===null;){if(h.return===null||h.return===e)break t;h=h.return}h.sibling.return=h.return,h=h.sibling}e.stateNode=u;t:switch(ne(u,s,a),s){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&on(e)}}return Lt(e),Po(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,n),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==a&&on(e);else{if(typeof a!="string"&&e.stateNode===null)throw Error(r(166));if(t=ut.current,ki(e)){if(t=e.stateNode,n=e.memoizedProps,a=null,s=It,s!==null)switch(s.tag){case 27:case 5:a=s.memoizedProps}t[$t]=e,t=!!(t.nodeValue===n||a!==null&&a.suppressHydrationWarning===!0||Bm(t.nodeValue,n)),t||Tn(e,!0)}else t=_s(t).createTextNode(a),t[$t]=e,e.stateNode=t}return Lt(e),null;case 31:if(n=e.memoizedState,t===null||t.memoizedState!==null){if(a=ki(e),n!==null){if(t===null){if(!a)throw Error(r(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(r(557));t[$t]=e}else ai(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Lt(e),t=!1}else n=so(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),t=!0;if(!t)return e.flags&256?(Te(e),e):(Te(e),null);if((e.flags&128)!==0)throw Error(r(558))}return Lt(e),null;case 13:if(a=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(s=ki(e),a!==null&&a.dehydrated!==null){if(t===null){if(!s)throw Error(r(318));if(s=e.memoizedState,s=s!==null?s.dehydrated:null,!s)throw Error(r(317));s[$t]=e}else ai(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Lt(e),s=!1}else s=so(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=s),s=!0;if(!s)return e.flags&256?(Te(e),e):(Te(e),null)}return Te(e),(e.flags&128)!==0?(e.lanes=n,e):(n=a!==null,t=t!==null&&t.memoizedState!==null,n&&(a=e.child,s=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(s=a.alternate.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==s&&(a.flags|=2048)),n!==t&&n&&(e.child.flags|=8192),gs(e,e.updateQueue),Lt(e),null);case 4:return Vt(),t===null&&vu(e.stateNode.containerInfo),Lt(e),null;case 10:return an(e.type),Lt(e),null;case 19:if(k(Bt),a=e.memoizedState,a===null)return Lt(e),null;if(s=(e.flags&128)!==0,u=a.rendering,u===null)if(s)qa(a,!1);else{if(Ot!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(u=as(t),u!==null){for(e.flags|=128,qa(a,!1),t=u.updateQueue,e.updateQueue=t,gs(e,t),e.subtreeFlags=0,t=n,n=e.child;n!==null;)yd(n,t),n=n.sibling;return X(Bt,Bt.current&1|2),pt&&en(e,a.treeForkCount),e.child}t=t.sibling}a.tail!==null&&ge()>js&&(e.flags|=128,s=!0,qa(a,!1),e.lanes=4194304)}else{if(!s)if(t=as(u),t!==null){if(e.flags|=128,s=!0,t=t.updateQueue,e.updateQueue=t,gs(e,t),qa(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!pt)return Lt(e),null}else 2*ge()-a.renderingStartTime>js&&n!==536870912&&(e.flags|=128,s=!0,qa(a,!1),e.lanes=4194304);a.isBackwards?(u.sibling=e.child,e.child=u):(t=a.last,t!==null?t.sibling=u:e.child=u,a.last=u)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=ge(),t.sibling=null,n=Bt.current,X(Bt,s?n&1|2:n&1),pt&&en(e,a.treeForkCount),t):(Lt(e),null);case 22:case 23:return Te(e),bo(),a=e.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(e.flags|=8192):a&&(e.flags|=8192),a?(n&536870912)!==0&&(e.flags&128)===0&&(Lt(e),e.subtreeFlags&6&&(e.flags|=8192)):Lt(e),n=e.updateQueue,n!==null&&gs(e,n.retryQueue),n=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(n=t.memoizedState.cachePool.pool),a=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(a=e.memoizedState.cachePool.pool),a!==n&&(e.flags|=2048),t!==null&&k(ri),null;case 24:return n=null,t!==null&&(n=t.memoizedState.cache),e.memoizedState.cache!==n&&(e.flags|=2048),an(Yt),Lt(e),null;case 25:return null;case 30:return null}throw Error(r(156,e.tag))}function sx(t,e){switch(ao(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return an(Yt),Vt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return Dl(e),null;case 31:if(e.memoizedState!==null){if(Te(e),e.alternate===null)throw Error(r(340));ai()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(Te(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(r(340));ai()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return k(Bt),null;case 4:return Vt(),null;case 10:return an(e.type),null;case 22:case 23:return Te(e),bo(),t!==null&&k(ri),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return an(Yt),null;case 25:return null;default:return null}}function Gh(t,e){switch(ao(e),e.tag){case 3:an(Yt),Vt();break;case 26:case 27:case 5:Dl(e);break;case 4:Vt();break;case 31:e.memoizedState!==null&&Te(e);break;case 13:Te(e);break;case 19:k(Bt);break;case 10:an(e.type);break;case 22:case 23:Te(e),bo(),t!==null&&k(ri);break;case 24:an(Yt)}}function Ga(t,e){try{var n=e.updateQueue,a=n!==null?n.lastEffect:null;if(a!==null){var s=a.next;n=s;do{if((n.tag&t)===t){a=void 0;var u=n.create,h=n.inst;a=u(),h.destroy=a}n=n.next}while(n!==s)}}catch(x){Tt(e,e.return,x)}}function Ln(t,e,n){try{var a=e.updateQueue,s=a!==null?a.lastEffect:null;if(s!==null){var u=s.next;a=u;do{if((a.tag&t)===t){var h=a.inst,x=h.destroy;if(x!==void 0){h.destroy=void 0,s=e;var S=n,E=x;try{E()}catch(_){Tt(s,S,_)}}}a=a.next}while(a!==u)}}catch(_){Tt(e,e.return,_)}}function Xh(t){var e=t.updateQueue;if(e!==null){var n=t.stateNode;try{Rd(e,n)}catch(a){Tt(t,t.return,a)}}}function Zh(t,e,n){n.props=di(t.type,t.memoizedProps),n.state=t.memoizedState;try{n.componentWillUnmount()}catch(a){Tt(t,e,a)}}function Xa(t,e){try{var n=t.ref;if(n!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof n=="function"?t.refCleanup=n(a):n.current=a}}catch(s){Tt(t,e,s)}}function Ke(t,e){var n=t.ref,a=t.refCleanup;if(n!==null)if(typeof a=="function")try{a()}catch(s){Tt(t,e,s)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(s){Tt(t,e,s)}else n.current=null}function Qh(t){var e=t.type,n=t.memoizedProps,a=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":n.autoFocus&&a.focus();break t;case"img":n.src?a.src=n.src:n.srcSet&&(a.srcset=n.srcSet)}}catch(s){Tt(t,t.return,s)}}function $o(t,e,n){try{var a=t.stateNode;Dx(a,t.type,n,e),a[ce]=e}catch(s){Tt(t,t.return,s)}}function Kh(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Vn(t.type)||t.tag===4}function Io(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||Kh(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Vn(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function tu(t,e,n){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(t,e):(e=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,e.appendChild(t),n=n._reactRootContainer,n!=null||e.onclick!==null||(e.onclick=$e));else if(a!==4&&(a===27&&Vn(t.type)&&(n=t.stateNode,e=null),t=t.child,t!==null))for(tu(t,e,n),t=t.sibling;t!==null;)tu(t,e,n),t=t.sibling}function xs(t,e,n){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?n.insertBefore(t,e):n.appendChild(t);else if(a!==4&&(a===27&&Vn(t.type)&&(n=t.stateNode),t=t.child,t!==null))for(xs(t,e,n),t=t.sibling;t!==null;)xs(t,e,n),t=t.sibling}function Wh(t){var e=t.stateNode,n=t.memoizedProps;try{for(var a=t.type,s=e.attributes;s.length;)e.removeAttributeNode(s[0]);ne(e,a,n),e[$t]=t,e[ce]=n}catch(u){Tt(t,t.return,u)}}var un=!1,Xt=!1,eu=!1,Jh=typeof WeakSet=="function"?WeakSet:Set,Pt=null;function rx(t,e){if(t=t.containerInfo,ju=Us,t=rd(t),Kr(t)){if("selectionStart"in t)var n={start:t.selectionStart,end:t.selectionEnd};else t:{n=(n=t.ownerDocument)&&n.defaultView||window;var a=n.getSelection&&n.getSelection();if(a&&a.rangeCount!==0){n=a.anchorNode;var s=a.anchorOffset,u=a.focusNode;a=a.focusOffset;try{n.nodeType,u.nodeType}catch{n=null;break t}var h=0,x=-1,S=-1,E=0,_=0,O=t,D=null;e:for(;;){for(var L;O!==n||s!==0&&O.nodeType!==3||(x=h+s),O!==u||a!==0&&O.nodeType!==3||(S=h+a),O.nodeType===3&&(h+=O.nodeValue.length),(L=O.firstChild)!==null;)D=O,O=L;for(;;){if(O===t)break e;if(D===n&&++E===s&&(x=h),D===u&&++_===a&&(S=h),(L=O.nextSibling)!==null)break;O=D,D=O.parentNode}O=L}n=x===-1||S===-1?null:{start:x,end:S}}else n=null}n=n||{start:0,end:0}}else n=null;for(Tu={focusedElem:t,selectionRange:n},Us=!1,Pt=e;Pt!==null;)if(e=Pt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,Pt=t;else for(;Pt!==null;){switch(e=Pt,u=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(n=0;n title"))),ne(u,a,n),u[$t]=t,Ft(u),a=u;break t;case"link":var h=ep("link","href",s).get(a+(n.href||""));if(h){for(var x=0;xMt&&(h=Mt,Mt=et,et=h);var C=ld(x,et),j=ld(x,Mt);if(C&&j&&(L.rangeCount!==1||L.anchorNode!==C.node||L.anchorOffset!==C.offset||L.focusNode!==j.node||L.focusOffset!==j.offset)){var M=O.createRange();M.setStart(C.node,C.offset),L.removeAllRanges(),et>Mt?(L.addRange(M),L.extend(j.node,j.offset)):(M.setEnd(j.node,j.offset),L.addRange(M))}}}}for(O=[],L=x;L=L.parentNode;)L.nodeType===1&&O.push({element:L,left:L.scrollLeft,top:L.scrollTop});for(typeof x.focus=="function"&&x.focus(),x=0;xn?32:n,z.T=null,n=ou,ou=null;var u=Rn,h=mn;if(Kt=0,Fi=Rn=null,mn=0,(bt&6)!==0)throw Error(r(331));var x=bt;if(bt|=4,sm(u.current),im(u,u.current,h,n),bt=x,Fa(0,!1),xe&&typeof xe.onPostCommitFiberRoot=="function")try{xe.onPostCommitFiberRoot(ma,u)}catch{}return!0}finally{B.p=s,z.T=a,Am(t,e)}}function Mm(t,e,n){e=Le(n,e),e=Yo(t.stateNode,e,2),t=En(t,e,2),t!==null&&(ya(t,2),We(t))}function Tt(t,e,n){if(t.tag===3)Mm(t,t,n);else for(;e!==null;){if(e.tag===3){Mm(e,t,n);break}else if(e.tag===1){var a=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(zn===null||!zn.has(a))){t=Le(n,t),n=Eh(2),a=En(e,n,2),a!==null&&(Dh(n,a,e,t),ya(a,2),We(a));break}}e=e.return}}function du(t,e,n){var a=t.pingCache;if(a===null){a=t.pingCache=new cx;var s=new Set;a.set(e,s)}else s=a.get(e),s===void 0&&(s=new Set,a.set(e,s));s.has(n)||(au=!0,s.add(n),t=px.bind(null,t,e,n),e.then(t,t))}function px(t,e,n){var a=t.pingCache;a!==null&&a.delete(e),t.pingedLanes|=t.suspendedLanes&n,t.warmLanes&=~n,Et===t&&(ht&n)===n&&(Ot===4||Ot===3&&(ht&62914560)===ht&&300>ge()-Ss?(bt&2)===0&&Pi(t,0):lu|=n,Ji===ht&&(Ji=0)),We(t)}function Em(t,e){e===0&&(e=Sf()),t=ni(t,e),t!==null&&(ya(t,e),We(t))}function yx(t){var e=t.memoizedState,n=0;e!==null&&(n=e.retryLane),Em(t,n)}function gx(t,e){var n=0;switch(t.tag){case 31:case 13:var a=t.stateNode,s=t.memoizedState;s!==null&&(n=s.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(r(314))}a!==null&&a.delete(e),Em(t,n)}function xx(t,e){return Ar(t,e)}var Ds=null,Ii=null,hu=!1,ws=!1,mu=!1,kn=0;function We(t){t!==Ii&&t.next===null&&(Ii===null?Ds=Ii=t:Ii=Ii.next=t),ws=!0,hu||(hu=!0,bx())}function Fa(t,e){if(!mu&&ws){mu=!0;do for(var n=!1,a=Ds;a!==null;){if(t!==0){var s=a.pendingLanes;if(s===0)var u=0;else{var h=a.suspendedLanes,x=a.pingedLanes;u=(1<<31-ve(42|t)+1)-1,u&=s&~(h&~x),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(n=!0,Nm(a,u))}else u=ht,u=zl(a,a===Et?u:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(u&3)===0||pa(a,u)||(n=!0,Nm(a,u));a=a.next}while(n);mu=!1}}function vx(){Dm()}function Dm(){ws=hu=!1;var t=0;kn!==0&&Lx()&&(t=kn);for(var e=ge(),n=null,a=Ds;a!==null;){var s=a.next,u=wm(a,e);u===0?(a.next=null,n===null?Ds=s:n.next=s,s===null&&(Ii=n)):(n=a,(t!==0||(u&3)!==0)&&(ws=!0)),a=s}Kt!==0&&Kt!==5||Fa(t),kn!==0&&(kn=0)}function wm(t,e){for(var n=t.suspendedLanes,a=t.pingedLanes,s=t.expirationTimes,u=t.pendingLanes&-62914561;0x)break;var _=S.transferSize,O=S.initiatorType;_&&Um(O)&&(S=S.responseEnd,h+=_*(S"u"?null:document;function Pm(t,e,n){var a=ta;if(a&&typeof e=="string"&&e){var s=De(e);s='link[rel="'+t+'"][href="'+s+'"]',typeof n=="string"&&(s+='[crossorigin="'+n+'"]'),Fm.has(s)||(Fm.add(s),t={rel:t,crossOrigin:n,href:e},a.querySelector(s)===null&&(e=a.createElement("link"),ne(e,"link",t),Ft(e),a.head.appendChild(e)))}}function Ux(t){pn.D(t),Pm("dns-prefetch",t,null)}function Hx(t,e){pn.C(t,e),Pm("preconnect",t,e)}function Yx(t,e,n){pn.L(t,e,n);var a=ta;if(a&&t&&e){var s='link[rel="preload"][as="'+De(e)+'"]';e==="image"&&n&&n.imageSrcSet?(s+='[imagesrcset="'+De(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(s+='[imagesizes="'+De(n.imageSizes)+'"]')):s+='[href="'+De(t)+'"]';var u=s;switch(e){case"style":u=ea(t);break;case"script":u=na(t)}ke.has(u)||(t=b({rel:"preload",href:e==="image"&&n&&n.imageSrcSet?void 0:t,as:e},n),ke.set(u,t),a.querySelector(s)!==null||e==="style"&&a.querySelector(tl(u))||e==="script"&&a.querySelector(el(u))||(e=a.createElement("link"),ne(e,"link",t),Ft(e),a.head.appendChild(e)))}}function qx(t,e){pn.m(t,e);var n=ta;if(n&&t){var a=e&&typeof e.as=="string"?e.as:"script",s='link[rel="modulepreload"][as="'+De(a)+'"][href="'+De(t)+'"]',u=s;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=na(t)}if(!ke.has(u)&&(t=b({rel:"modulepreload",href:t},e),ke.set(u,t),n.querySelector(s)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(el(u)))return}a=n.createElement("link"),ne(a,"link",t),Ft(a),n.head.appendChild(a)}}}function Gx(t,e,n){pn.S(t,e,n);var a=ta;if(a&&t){var s=Ti(a).hoistableStyles,u=ea(t);e=e||"default";var h=s.get(u);if(!h){var x={loading:0,preload:null};if(h=a.querySelector(tl(u)))x.loading=5;else{t=b({rel:"stylesheet",href:t,"data-precedence":e},n),(n=ke.get(u))&&Lu(t,n);var S=h=a.createElement("link");Ft(S),ne(S,"link",t),S._p=new Promise(function(E,_){S.onload=E,S.onerror=_}),S.addEventListener("load",function(){x.loading|=1}),S.addEventListener("error",function(){x.loading|=2}),x.loading|=4,Rs(h,e,a)}h={type:"stylesheet",instance:h,count:1,state:x},s.set(u,h)}}}function Xx(t,e){pn.X(t,e);var n=ta;if(n&&t){var a=Ti(n).hoistableScripts,s=na(t),u=a.get(s);u||(u=n.querySelector(el(s)),u||(t=b({src:t,async:!0},e),(e=ke.get(s))&&Nu(t,e),u=n.createElement("script"),Ft(u),ne(u,"link",t),n.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(s,u))}}function Zx(t,e){pn.M(t,e);var n=ta;if(n&&t){var a=Ti(n).hoistableScripts,s=na(t),u=a.get(s);u||(u=n.querySelector(el(s)),u||(t=b({src:t,async:!0,type:"module"},e),(e=ke.get(s))&&Nu(t,e),u=n.createElement("script"),Ft(u),ne(u,"link",t),n.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(s,u))}}function $m(t,e,n,a){var s=(s=ut.current)?zs(s):null;if(!s)throw Error(r(446));switch(t){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(e=ea(n.href),n=Ti(s).hoistableStyles,a=n.get(e),a||(a={type:"style",instance:null,count:0,state:null},n.set(e,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){t=ea(n.href);var u=Ti(s).hoistableStyles,h=u.get(t);if(h||(s=s.ownerDocument||s,h={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(t,h),(u=s.querySelector(tl(t)))&&!u._p&&(h.instance=u,h.state.loading=5),ke.has(t)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},ke.set(t,n),u||Qx(s,t,n,h.state))),e&&a===null)throw Error(r(528,""));return h}if(e&&a!==null)throw Error(r(529,""));return null;case"script":return e=n.async,n=n.src,typeof n=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=na(n),n=Ti(s).hoistableScripts,a=n.get(e),a||(a={type:"script",instance:null,count:0,state:null},n.set(e,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,t))}}function ea(t){return'href="'+De(t)+'"'}function tl(t){return'link[rel="stylesheet"]['+t+"]"}function Im(t){return b({},t,{"data-precedence":t.precedence,precedence:null})}function Qx(t,e,n,a){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?a.loading=1:(e=t.createElement("link"),a.preload=e,e.addEventListener("load",function(){return a.loading|=1}),e.addEventListener("error",function(){return a.loading|=2}),ne(e,"link",n),Ft(e),t.head.appendChild(e))}function na(t){return'[src="'+De(t)+'"]'}function el(t){return"script[async]"+t}function tp(t,e,n){if(e.count++,e.instance===null)switch(e.type){case"style":var a=t.querySelector('style[data-href~="'+De(n.href)+'"]');if(a)return e.instance=a,Ft(a),a;var s=b({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Ft(a),ne(a,"style",s),Rs(a,n.precedence,t),e.instance=a;case"stylesheet":s=ea(n.href);var u=t.querySelector(tl(s));if(u)return e.state.loading|=4,e.instance=u,Ft(u),u;a=Im(n),(s=ke.get(s))&&Lu(a,s),u=(t.ownerDocument||t).createElement("link"),Ft(u);var h=u;return h._p=new Promise(function(x,S){h.onload=x,h.onerror=S}),ne(u,"link",a),e.state.loading|=4,Rs(u,n.precedence,t),e.instance=u;case"script":return u=na(n.src),(s=t.querySelector(el(u)))?(e.instance=s,Ft(s),s):(a=n,(s=ke.get(u))&&(a=b({},n),Nu(a,s)),t=t.ownerDocument||t,s=t.createElement("script"),Ft(s),ne(s,"link",a),t.head.appendChild(s),e.instance=s);case"void":return null;default:throw Error(r(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(a=e.instance,e.state.loading|=4,Rs(a,n.precedence,t));return e.instance}function Rs(t,e,n){for(var a=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),s=a.length?a[a.length-1]:null,u=s,h=0;h title"):null)}function Kx(t,e,n){if(n===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;return e.rel==="stylesheet"?(t=e.disabled,typeof e.precedence=="string"&&t==null):!0;case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function ip(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Wx(t,e,n,a){if(n.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(n.state.loading&4)===0){if(n.instance===null){var s=ea(a.href),u=e.querySelector(tl(s));if(u){e=u._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=ks.bind(t),e.then(t,t)),n.state.loading|=4,n.instance=u,Ft(u);return}u=e.ownerDocument||e,a=Im(a),(s=ke.get(s))&&Lu(a,s),u=u.createElement("link"),Ft(u);var h=u;h._p=new Promise(function(x,S){h.onload=x,h.onerror=S}),ne(u,"link",a),n.instance=u}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(n,e),(e=n.state.preload)&&(n.state.loading&3)===0&&(t.count++,n=ks.bind(t),e.addEventListener("load",n),e.addEventListener("error",n))}}var _u=0;function Jx(t,e){return t.stylesheets&&t.count===0&&Bs(t,t.stylesheets),0_u?50:800)+e);return t.unsuspend=n,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(s)}}:null}function ks(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Bs(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Vs=null;function Bs(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Vs=new Map,e.forEach(Fx,t),Vs=null,ks.call(t))}function Fx(t,e){if(!(e.state.loading&4)){var n=Vs.get(t);if(n)var a=n.get(null);else{n=new Map,Vs.set(t,n);for(var s=t.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(l){console.error(l)}}return i(),Yu.exports=hv(),Yu.exports}var pv=mv();const yv=()=>c.jsx("svg",{style:{display:"none"},children:c.jsxs("defs",{children:[c.jsxs("g",{id:"icon-clipboard",children:[c.jsx("rect",{x:"-12",y:"-16",width:"24",height:"32",rx:"2",fill:"none",stroke:"currentColor",strokeWidth:"2"}),c.jsx("rect",{x:"-6",y:"-20",width:"12",height:"6",rx:"1",fill:"none",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-7",y1:"-4",x2:"7",y2:"-4",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-7",y1:"3",x2:"7",y2:"3",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-7",y1:"10",x2:"4",y2:"10",stroke:"currentColor",strokeWidth:"2"})]}),c.jsxs("g",{id:"icon-books",children:[c.jsx("rect",{x:"-14",y:"-8",width:"8",height:"22",rx:"1",fill:"none",stroke:"currentColor",strokeWidth:"2",transform:"rotate(-10)"}),c.jsx("rect",{x:"-4",y:"-10",width:"8",height:"24",rx:"1",fill:"none",stroke:"currentColor",strokeWidth:"2"}),c.jsx("rect",{x:"6",y:"-8",width:"8",height:"22",rx:"1",fill:"none",stroke:"currentColor",strokeWidth:"2",transform:"rotate(10)"})]}),c.jsxs("g",{id:"icon-gear",children:[c.jsx("circle",{cx:"0",cy:"0",r:"7",fill:"none",stroke:"currentColor",strokeWidth:"2"}),c.jsx("path",{d:"M0,-16 L3,-12 L3,-10 L-3,-10 L-3,-12 Z",fill:"currentColor"}),c.jsx("path",{d:"M0,16 L3,12 L3,10 L-3,10 L-3,12 Z",fill:"currentColor"}),c.jsx("path",{d:"M-16,0 L-12,3 L-10,3 L-10,-3 L-12,-3 Z",fill:"currentColor"}),c.jsx("path",{d:"M16,0 L12,3 L10,3 L10,-3 L12,-3 Z",fill:"currentColor"}),c.jsx("path",{d:"M-11,-11 L-9,-8 L-7,-9 L-9,-12 Z",fill:"currentColor",transform:"rotate(0)"}),c.jsx("path",{d:"M11,-11 L9,-8 L7,-9 L9,-12 Z",fill:"currentColor"}),c.jsx("path",{d:"M-11,11 L-9,8 L-7,9 L-9,12 Z",fill:"currentColor"}),c.jsx("path",{d:"M11,11 L9,8 L7,9 L9,12 Z",fill:"currentColor"})]}),c.jsxs("g",{id:"icon-flask",children:[c.jsx("path",{d:"M-5,-16 L-5,-4 L-14,14 L14,14 L5,-4 L5,-16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinejoin:"round"}),c.jsx("line",{x1:"-7",y1:"-16",x2:"7",y2:"-16",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-9",y1:"6",x2:"9",y2:"6",stroke:"currentColor",strokeWidth:"2",strokeDasharray:"3 2"})]}),c.jsx("g",{id:"icon-lambda",children:c.jsx("text",{x:"0",y:"8",fontFamily:"Georgia, serif",fontSize:"40",fontWeight:"400",fill:"currentColor",textAnchor:"middle",children:"λ"})}),c.jsx("g",{id:"icon-lightning",children:c.jsx("polygon",{points:"2,-16 -8,2 -1,2 -4,16 8,-2 1,-2",fill:"currentColor",stroke:"currentColor",strokeWidth:"1",strokeLinejoin:"round"})}),c.jsxs("g",{id:"icon-search",children:[c.jsx("circle",{cx:"-3",cy:"-3",r:"12",fill:"none",stroke:"currentColor",strokeWidth:"2.5"}),c.jsx("line",{x1:"6",y1:"6",x2:"16",y2:"16",stroke:"currentColor",strokeWidth:"3",strokeLinecap:"round"})]}),c.jsxs("g",{id:"icon-link",children:[c.jsx("ellipse",{cx:"-6",cy:"0",rx:"8",ry:"12",fill:"none",stroke:"currentColor",strokeWidth:"2.5",transform:"rotate(-45)"}),c.jsx("ellipse",{cx:"6",cy:"0",rx:"8",ry:"12",fill:"none",stroke:"currentColor",strokeWidth:"2.5",transform:"rotate(-45)"})]}),c.jsx("g",{id:"icon-wrench",children:c.jsx("path",{d:"M-6,-16 C-12,-10 -12,-2 -6,4 L8,18 L14,12 L0,-2 C6,-8 6,-14 0,-16 L-2,-10 L-6,-10 Z",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinejoin:"round"})}),c.jsxs("g",{id:"icon-upload",children:[c.jsx("line",{x1:"0",y1:"12",x2:"0",y2:"-8",stroke:"currentColor",strokeWidth:"3",strokeLinecap:"round"}),c.jsx("polyline",{points:"-8,-2 0,-12 8,-2",fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinecap:"round",strokeLinejoin:"round"}),c.jsx("line",{x1:"-12",y1:"16",x2:"12",y2:"16",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round"})]}),c.jsxs("g",{id:"icon-book",children:[c.jsx("path",{d:"M0,-12 L0,14",stroke:"currentColor",strokeWidth:"2"}),c.jsx("path",{d:"M0,-12 C-6,-14 -12,-12 -16,-8 L-16,12 C-12,8 -6,10 0,14",fill:"none",stroke:"currentColor",strokeWidth:"2"}),c.jsx("path",{d:"M0,-12 C6,-14 12,-12 16,-8 L16,12 C12,8 6,10 0,14",fill:"none",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-12",y1:"-4",x2:"-4",y2:"-2",stroke:"currentColor",strokeWidth:"1.5"}),c.jsx("line",{x1:"-12",y1:"2",x2:"-4",y2:"4",stroke:"currentColor",strokeWidth:"1.5"}),c.jsx("line",{x1:"12",y1:"-4",x2:"4",y2:"-2",stroke:"currentColor",strokeWidth:"1.5"}),c.jsx("line",{x1:"12",y1:"2",x2:"4",y2:"4",stroke:"currentColor",strokeWidth:"1.5"})]}),c.jsxs("g",{id:"icon-document",children:[c.jsx("path",{d:"M-10,-16 L6,-16 L14,-8 L14,16 L-10,16 Z",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinejoin:"round"}),c.jsx("path",{d:"M6,-16 L6,-8 L14,-8",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinejoin:"round"}),c.jsx("line",{x1:"-5",y1:"0",x2:"9",y2:"0",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-5",y1:"6",x2:"9",y2:"6",stroke:"currentColor",strokeWidth:"2"}),c.jsx("line",{x1:"-5",y1:"12",x2:"4",y2:"12",stroke:"currentColor",strokeWidth:"2"})]}),c.jsxs("g",{id:"icon-chat",children:[c.jsx("path",{d:"M-14,-10 L14,-10 C16,-10 16,-10 16,-8 L16,6 C16,8 16,8 14,8 L4,8 L-2,16 L-2,8 L-14,8 C-16,8 -16,8 -16,6 L-16,-8 C-16,-10 -16,-10 -14,-10 Z",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinejoin:"round"}),c.jsx("circle",{cx:"-7",cy:"-1",r:"2",fill:"currentColor"}),c.jsx("circle",{cx:"0",cy:"-1",r:"2",fill:"currentColor"}),c.jsx("circle",{cx:"7",cy:"-1",r:"2",fill:"currentColor"})]})]})}),Vc=Y.createContext({});function Bc(i){const l=Y.useRef(null);return l.current===null&&(l.current=i()),l.current}const t0=typeof window<"u",e0=t0?Y.useLayoutEffect:Y.useEffect,pr=Y.createContext(null);function Uc(i,l){i.indexOf(l)===-1&&i.push(l)}function Hc(i,l){const o=i.indexOf(l);o>-1&&i.splice(o,1)}const Fe=(i,l,o)=>o>l?l:o{};const gn={},n0=i=>/^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(i);function i0(i){return typeof i=="object"&&i!==null}const a0=i=>/^0[^.\s]+$/u.test(i);function qc(i){let l;return()=>(l===void 0&&(l=i()),l)}const Ue=i=>i,gv=(i,l)=>o=>l(i(o)),Al=(...i)=>i.reduce(gv),vl=(i,l,o)=>{const r=l-i;return r===0?1:(o-i)/r};class Gc{constructor(){this.subscriptions=[]}add(l){return Uc(this.subscriptions,l),()=>Hc(this.subscriptions,l)}notify(l,o,r){const f=this.subscriptions.length;if(f)if(f===1)this.subscriptions[0](l,o,r);else for(let m=0;mi*1e3,Be=i=>i/1e3;function l0(i,l){return l?i*(1e3/l):0}const s0=(i,l,o)=>(((1-3*o+3*l)*i+(3*o-6*l))*i+3*l)*i,xv=1e-7,vv=12;function bv(i,l,o,r,f){let m,d,p=0;do d=l+(o-l)/2,m=s0(d,r,f)-i,m>0?o=d:l=d;while(Math.abs(m)>xv&&++pbv(m,0,1,i,o);return m=>m===0||m===1?m:s0(f(m),l,r)}const r0=i=>l=>l<=.5?i(2*l)/2:(2-i(2*(1-l)))/2,o0=i=>l=>1-i(1-l),u0=Cl(.33,1.53,.69,.99),Xc=o0(u0),c0=r0(Xc),f0=i=>(i*=2)<1?.5*Xc(i):.5*(2-Math.pow(2,-10*(i-1))),Zc=i=>1-Math.sin(Math.acos(i)),d0=o0(Zc),h0=r0(Zc),Sv=Cl(.42,0,1,1),jv=Cl(0,0,.58,1),m0=Cl(.42,0,.58,1),Tv=i=>Array.isArray(i)&&typeof i[0]!="number",p0=i=>Array.isArray(i)&&typeof i[0]=="number",Av={linear:Ue,easeIn:Sv,easeInOut:m0,easeOut:jv,circIn:Zc,circInOut:h0,circOut:d0,backIn:Xc,backInOut:c0,backOut:u0,anticipate:f0},Cv=i=>typeof i=="string",Ep=i=>{if(p0(i)){Yc(i.length===4);const[l,o,r,f]=i;return Cl(l,o,r,f)}else if(Cv(i))return Av[i];return i},Qs=["setup","read","resolveKeyframes","preUpdate","update","preRender","render","postRender"];function Mv(i,l){let o=new Set,r=new Set,f=!1,m=!1;const d=new WeakSet;let p={delta:0,timestamp:0,isProcessing:!1};function y(v){d.has(v)&&(g.schedule(v),i()),v(p)}const g={schedule:(v,b=!1,T=!1)=>{const N=T&&f?o:r;return b&&d.add(v),N.has(v)||N.add(v),v},cancel:v=>{r.delete(v),d.delete(v)},process:v=>{if(p=v,f){m=!0;return}f=!0,[o,r]=[r,o],o.forEach(y),o.clear(),f=!1,m&&(m=!1,g.process(v))}};return g}const Ev=40;function y0(i,l){let o=!1,r=!0;const f={delta:0,timestamp:0,isProcessing:!1},m=()=>o=!0,d=Qs.reduce((V,Z)=>(V[Z]=Mv(m),V),{}),{setup:p,read:y,resolveKeyframes:g,preUpdate:v,update:b,preRender:T,render:w,postRender:N}=d,H=()=>{const V=gn.useManualTiming?f.timestamp:performance.now();o=!1,gn.useManualTiming||(f.delta=r?1e3/60:Math.max(Math.min(V-f.timestamp,Ev),1)),f.timestamp=V,f.isProcessing=!0,p.process(f),y.process(f),g.process(f),v.process(f),b.process(f),T.process(f),w.process(f),N.process(f),f.isProcessing=!1,o&&l&&(r=!1,i(H))},G=()=>{o=!0,r=!0,f.isProcessing||i(H)};return{schedule:Qs.reduce((V,Z)=>{const Q=d[Z];return V[Z]=(nt,F=!1,K=!1)=>(o||G(),Q.schedule(nt,F,K)),V},{}),cancel:V=>{for(let Z=0;Z(ar===void 0&&oe.set(ie.isProcessing||gn.useManualTiming?ie.timestamp:performance.now()),ar),set:i=>{ar=i,queueMicrotask(Dv)}},g0=i=>l=>typeof l=="string"&&l.startsWith(i),x0=g0("--"),wv=g0("var(--"),Qc=i=>wv(i)?Lv.test(i.split("/*")[0].trim()):!1,Lv=/var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu;function Dp(i){return typeof i!="string"?!1:i.split("/*")[0].includes("var(--")}const ca={test:i=>typeof i=="number",parse:parseFloat,transform:i=>i},bl={...ca,transform:i=>Fe(0,1,i)},Ks={...ca,default:1},ml=i=>Math.round(i*1e5)/1e5,Kc=/-?(?:\d+(?:\.\d+)?|\.\d+)/gu;function Nv(i){return i==null}const _v=/^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu,Wc=(i,l)=>o=>!!(typeof o=="string"&&_v.test(o)&&o.startsWith(i)||l&&!Nv(o)&&Object.prototype.hasOwnProperty.call(o,l)),v0=(i,l,o)=>r=>{if(typeof r!="string")return r;const[f,m,d,p]=r.match(Kc);return{[i]:parseFloat(f),[l]:parseFloat(m),[o]:parseFloat(d),alpha:p!==void 0?parseFloat(p):1}},zv=i=>Fe(0,255,i),Qu={...ca,transform:i=>Math.round(zv(i))},gi={test:Wc("rgb","red"),parse:v0("red","green","blue"),transform:({red:i,green:l,blue:o,alpha:r=1})=>"rgba("+Qu.transform(i)+", "+Qu.transform(l)+", "+Qu.transform(o)+", "+ml(bl.transform(r))+")"};function Rv(i){let l="",o="",r="",f="";return i.length>5?(l=i.substring(1,3),o=i.substring(3,5),r=i.substring(5,7),f=i.substring(7,9)):(l=i.substring(1,2),o=i.substring(2,3),r=i.substring(3,4),f=i.substring(4,5),l+=l,o+=o,r+=r,f+=f),{red:parseInt(l,16),green:parseInt(o,16),blue:parseInt(r,16),alpha:f?parseInt(f,16)/255:1}}const cc={test:Wc("#"),parse:Rv,transform:gi.transform},Ml=i=>({test:l=>typeof l=="string"&&l.endsWith(i)&&l.split(" ").length===1,parse:parseFloat,transform:l=>`${l}${i}`}),Gn=Ml("deg"),Je=Ml("%"),J=Ml("px"),Ov=Ml("vh"),kv=Ml("vw"),wp={...Je,parse:i=>Je.parse(i)/100,transform:i=>Je.transform(i*100)},aa={test:Wc("hsl","hue"),parse:v0("hue","saturation","lightness"),transform:({hue:i,saturation:l,lightness:o,alpha:r=1})=>"hsla("+Math.round(i)+", "+Je.transform(ml(l))+", "+Je.transform(ml(o))+", "+ml(bl.transform(r))+")"},Zt={test:i=>gi.test(i)||cc.test(i)||aa.test(i),parse:i=>gi.test(i)?gi.parse(i):aa.test(i)?aa.parse(i):cc.parse(i),transform:i=>typeof i=="string"?i:i.hasOwnProperty("red")?gi.transform(i):aa.transform(i),getAnimatableNone:i=>{const l=Zt.parse(i);return l.alpha=0,Zt.transform(l)}},Vv=/(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu;function Bv(i){return isNaN(i)&&typeof i=="string"&&(i.match(Kc)?.length||0)+(i.match(Vv)?.length||0)>0}const b0="number",S0="color",Uv="var",Hv="var(",Lp="${}",Yv=/var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu;function Sl(i){const l=i.toString(),o=[],r={color:[],number:[],var:[]},f=[];let m=0;const p=l.replace(Yv,y=>(Zt.test(y)?(r.color.push(m),f.push(S0),o.push(Zt.parse(y))):y.startsWith(Hv)?(r.var.push(m),f.push(Uv),o.push(y)):(r.number.push(m),f.push(b0),o.push(parseFloat(y))),++m,Lp)).split(Lp);return{values:o,split:p,indexes:r,types:f}}function j0(i){return Sl(i).values}function T0(i){const{split:l,types:o}=Sl(i),r=l.length;return f=>{let m="";for(let d=0;dtypeof i=="number"?0:Zt.test(i)?Zt.getAnimatableNone(i):i;function Gv(i){const l=j0(i);return T0(i)(l.map(qv))}const Kn={test:Bv,parse:j0,createTransformer:T0,getAnimatableNone:Gv};function Ku(i,l,o){return o<0&&(o+=1),o>1&&(o-=1),o<1/6?i+(l-i)*6*o:o<1/2?l:o<2/3?i+(l-i)*(2/3-o)*6:i}function Xv({hue:i,saturation:l,lightness:o,alpha:r}){i/=360,l/=100,o/=100;let f=0,m=0,d=0;if(!l)f=m=d=o;else{const p=o<.5?o*(1+l):o+l-o*l,y=2*o-p;f=Ku(y,p,i+1/3),m=Ku(y,p,i),d=Ku(y,p,i-1/3)}return{red:Math.round(f*255),green:Math.round(m*255),blue:Math.round(d*255),alpha:r}}function or(i,l){return o=>o>0?l:i}const zt=(i,l,o)=>i+(l-i)*o,Wu=(i,l,o)=>{const r=i*i,f=o*(l*l-r)+r;return f<0?0:Math.sqrt(f)},Zv=[cc,gi,aa],Qv=i=>Zv.find(l=>l.test(i));function Np(i){const l=Qv(i);if(!l)return!1;let o=l.parse(i);return l===aa&&(o=Xv(o)),o}const _p=(i,l)=>{const o=Np(i),r=Np(l);if(!o||!r)return or(i,l);const f={...o};return m=>(f.red=Wu(o.red,r.red,m),f.green=Wu(o.green,r.green,m),f.blue=Wu(o.blue,r.blue,m),f.alpha=zt(o.alpha,r.alpha,m),gi.transform(f))},fc=new Set(["none","hidden"]);function Kv(i,l){return fc.has(i)?o=>o<=0?i:l:o=>o>=1?l:i}function Wv(i,l){return o=>zt(i,l,o)}function Jc(i){return typeof i=="number"?Wv:typeof i=="string"?Qc(i)?or:Zt.test(i)?_p:Pv:Array.isArray(i)?A0:typeof i=="object"?Zt.test(i)?_p:Jv:or}function A0(i,l){const o=[...i],r=o.length,f=i.map((m,d)=>Jc(m)(m,l[d]));return m=>{for(let d=0;d{for(const m in r)o[m]=r[m](f);return o}}function Fv(i,l){const o=[],r={color:0,var:0,number:0};for(let f=0;f{const o=Kn.createTransformer(l),r=Sl(i),f=Sl(l);return r.indexes.var.length===f.indexes.var.length&&r.indexes.color.length===f.indexes.color.length&&r.indexes.number.length>=f.indexes.number.length?fc.has(i)&&!f.values.length||fc.has(l)&&!r.values.length?Kv(i,l):Al(A0(Fv(r,f),f.values),o):or(i,l)};function C0(i,l,o){return typeof i=="number"&&typeof l=="number"&&typeof o=="number"?zt(i,l,o):Jc(i)(i,l)}const $v=i=>{const l=({timestamp:o})=>i(o);return{start:(o=!0)=>Dt.update(l,o),stop:()=>Qn(l),now:()=>ie.isProcessing?ie.timestamp:oe.now()}},M0=(i,l,o=10)=>{let r="";const f=Math.max(Math.round(l/o),2);for(let m=0;m=ur?1/0:l}function Iv(i,l=100,o){const r=o({...i,keyframes:[0,l]}),f=Math.min(Fc(r),ur);return{type:"keyframes",ease:m=>r.next(f*m).value/l,duration:Be(f)}}const t2=5;function E0(i,l,o){const r=Math.max(l-t2,0);return l0(o-i(r),l-r)}const kt={stiffness:100,damping:10,mass:1,velocity:0,duration:800,bounce:.3,visualDuration:.3,restSpeed:{granular:.01,default:2},restDelta:{granular:.005,default:.5},minDuration:.01,maxDuration:10,minDamping:.05,maxDamping:1},Ju=.001;function e2({duration:i=kt.duration,bounce:l=kt.bounce,velocity:o=kt.velocity,mass:r=kt.mass}){let f,m,d=1-l;d=Fe(kt.minDamping,kt.maxDamping,d),i=Fe(kt.minDuration,kt.maxDuration,Be(i)),d<1?(f=g=>{const v=g*d,b=v*i,T=v-o,w=dc(g,d),N=Math.exp(-b);return Ju-T/w*N},m=g=>{const b=g*d*i,T=b*o+o,w=Math.pow(d,2)*Math.pow(g,2)*i,N=Math.exp(-b),H=dc(Math.pow(g,2),d);return(-f(g)+Ju>0?-1:1)*((T-w)*N)/H}):(f=g=>{const v=Math.exp(-g*i),b=(g-o)*i+1;return-Ju+v*b},m=g=>{const v=Math.exp(-g*i),b=(o-g)*(i*i);return v*b});const p=5/i,y=i2(f,m,p);if(i=yn(i),isNaN(y))return{stiffness:kt.stiffness,damping:kt.damping,duration:i};{const g=Math.pow(y,2)*r;return{stiffness:g,damping:d*2*Math.sqrt(r*g),duration:i}}}const n2=12;function i2(i,l,o){let r=o;for(let f=1;fi[o]!==void 0)}function s2(i){let l={velocity:kt.velocity,stiffness:kt.stiffness,damping:kt.damping,mass:kt.mass,isResolvedFromDuration:!1,...i};if(!zp(i,l2)&&zp(i,a2))if(i.visualDuration){const o=i.visualDuration,r=2*Math.PI/(o*1.2),f=r*r,m=2*Fe(.05,1,1-(i.bounce||0))*Math.sqrt(f);l={...l,mass:kt.mass,stiffness:f,damping:m}}else{const o=e2(i);l={...l,...o,mass:kt.mass},l.isResolvedFromDuration=!0}return l}function cr(i=kt.visualDuration,l=kt.bounce){const o=typeof i!="object"?{visualDuration:i,keyframes:[0,1],bounce:l}:i;let{restSpeed:r,restDelta:f}=o;const m=o.keyframes[0],d=o.keyframes[o.keyframes.length-1],p={done:!1,value:m},{stiffness:y,damping:g,mass:v,duration:b,velocity:T,isResolvedFromDuration:w}=s2({...o,velocity:-Be(o.velocity||0)}),N=T||0,H=g/(2*Math.sqrt(y*v)),G=d-m,U=Be(Math.sqrt(y/v)),q=Math.abs(G)<5;r||(r=q?kt.restSpeed.granular:kt.restSpeed.default),f||(f=q?kt.restDelta.granular:kt.restDelta.default);let V;if(H<1){const Q=dc(U,H);V=nt=>{const F=Math.exp(-H*U*nt);return d-F*((N+H*U*G)/Q*Math.sin(Q*nt)+G*Math.cos(Q*nt))}}else if(H===1)V=Q=>d-Math.exp(-U*Q)*(G+(N+U*G)*Q);else{const Q=U*Math.sqrt(H*H-1);V=nt=>{const F=Math.exp(-H*U*nt),K=Math.min(Q*nt,300);return d-F*((N+H*U*G)*Math.sinh(K)+Q*G*Math.cosh(K))/Q}}const Z={calculatedDuration:w&&b||null,next:Q=>{const nt=V(Q);if(w)p.done=Q>=b;else{let F=Q===0?N:0;H<1&&(F=Q===0?yn(N):E0(V,Q,nt));const K=Math.abs(F)<=r,it=Math.abs(d-nt)<=f;p.done=K&&it}return p.value=p.done?d:nt,p},toString:()=>{const Q=Math.min(Fc(Z),ur),nt=M0(F=>Z.next(Q*F).value,Q,30);return Q+"ms "+nt},toTransition:()=>{}};return Z}cr.applyToOptions=i=>{const l=Iv(i,100,cr);return i.ease=l.ease,i.duration=yn(l.duration),i.type="keyframes",i};function hc({keyframes:i,velocity:l=0,power:o=.8,timeConstant:r=325,bounceDamping:f=10,bounceStiffness:m=500,modifyTarget:d,min:p,max:y,restDelta:g=.5,restSpeed:v}){const b=i[0],T={done:!1,value:b},w=K=>p!==void 0&&Ky,N=K=>p===void 0?y:y===void 0||Math.abs(p-K)-H*Math.exp(-K/r),V=K=>U+q(K),Z=K=>{const it=q(K),yt=V(K);T.done=Math.abs(it)<=g,T.value=T.done?U:yt};let Q,nt;const F=K=>{w(T.value)&&(Q=K,nt=cr({keyframes:[T.value,N(T.value)],velocity:E0(V,K,T.value),damping:f,stiffness:m,restDelta:g,restSpeed:v}))};return F(0),{calculatedDuration:null,next:K=>{let it=!1;return!nt&&Q===void 0&&(it=!0,Z(K),F(K)),Q!==void 0&&K>=Q?nt.next(K-Q):(!it&&Z(K),T)}}}function r2(i,l,o){const r=[],f=o||gn.mix||C0,m=i.length-1;for(let d=0;dl[0];if(m===2&&l[0]===l[1])return()=>l[1];const d=i[0]===i[1];i[0]>i[m-1]&&(i=[...i].reverse(),l=[...l].reverse());const p=r2(l,r,f),y=p.length,g=v=>{if(d&&v1)for(;bg(Fe(i[0],i[m-1],v)):g}function u2(i,l){const o=i[i.length-1];for(let r=1;r<=l;r++){const f=vl(0,l,r);i.push(zt(o,1,f))}}function c2(i){const l=[0];return u2(l,i.length-1),l}function f2(i,l){return i.map(o=>o*l)}function d2(i,l){return i.map(()=>l||m0).splice(0,i.length-1)}function pl({duration:i=300,keyframes:l,times:o,ease:r="easeInOut"}){const f=Tv(r)?r.map(Ep):Ep(r),m={done:!1,value:l[0]},d=f2(o&&o.length===l.length?o:c2(l),i),p=o2(d,l,{ease:Array.isArray(f)?f:d2(l,f)});return{calculatedDuration:i,next:y=>(m.value=p(y),m.done=y>=i,m)}}const h2=i=>i!==null;function Pc(i,{repeat:l,repeatType:o="loop"},r,f=1){const m=i.filter(h2),p=f<0||l&&o!=="loop"&&l%2===1?0:m.length-1;return!p||r===void 0?m[p]:r}const m2={decay:hc,inertia:hc,tween:pl,keyframes:pl,spring:cr};function D0(i){typeof i.type=="string"&&(i.type=m2[i.type])}class $c{constructor(){this.updateFinished()}get finished(){return this._finished}updateFinished(){this._finished=new Promise(l=>{this.resolve=l})}notifyFinished(){this.resolve()}then(l,o){return this.finished.then(l,o)}}const p2=i=>i/100;class Ic extends $c{constructor(l){super(),this.state="idle",this.startTime=null,this.isStopped=!1,this.currentTime=0,this.holdTime=null,this.playbackSpeed=1,this.stop=()=>{const{motionValue:o}=this.options;o&&o.updatedAt!==oe.now()&&this.tick(oe.now()),this.isStopped=!0,this.state!=="idle"&&(this.teardown(),this.options.onStop?.())},this.options=l,this.initAnimation(),this.play(),l.autoplay===!1&&this.pause()}initAnimation(){const{options:l}=this;D0(l);const{type:o=pl,repeat:r=0,repeatDelay:f=0,repeatType:m,velocity:d=0}=l;let{keyframes:p}=l;const y=o||pl;y!==pl&&typeof p[0]!="number"&&(this.mixKeyframes=Al(p2,C0(p[0],p[1])),p=[0,100]);const g=y({...l,keyframes:p});m==="mirror"&&(this.mirroredGenerator=y({...l,keyframes:[...p].reverse(),velocity:-d})),g.calculatedDuration===null&&(g.calculatedDuration=Fc(g));const{calculatedDuration:v}=g;this.calculatedDuration=v,this.resolvedDuration=v+f,this.totalDuration=this.resolvedDuration*(r+1)-f,this.generator=g}updateTime(l){const o=Math.round(l-this.startTime)*this.playbackSpeed;this.holdTime!==null?this.currentTime=this.holdTime:this.currentTime=o}tick(l,o=!1){const{generator:r,totalDuration:f,mixKeyframes:m,mirroredGenerator:d,resolvedDuration:p,calculatedDuration:y}=this;if(this.startTime===null)return r.next(0);const{delay:g=0,keyframes:v,repeat:b,repeatType:T,repeatDelay:w,type:N,onUpdate:H,finalKeyframe:G}=this.options;this.speed>0?this.startTime=Math.min(this.startTime,l):this.speed<0&&(this.startTime=Math.min(l-f/this.speed,this.startTime)),o?this.currentTime=l:this.updateTime(l);const U=this.currentTime-g*(this.playbackSpeed>=0?1:-1),q=this.playbackSpeed>=0?U<0:U>f;this.currentTime=Math.max(U,0),this.state==="finished"&&this.holdTime===null&&(this.currentTime=f);let V=this.currentTime,Z=r;if(b){const K=Math.min(this.currentTime,f)/p;let it=Math.floor(K),yt=K%1;!yt&&K>=1&&(yt=1),yt===1&&it--,it=Math.min(it,b+1),it%2&&(T==="reverse"?(yt=1-yt,w&&(yt-=w/p)):T==="mirror"&&(Z=d)),V=Fe(0,1,yt)*p}const Q=q?{done:!1,value:v[0]}:Z.next(V);m&&(Q.value=m(Q.value));let{done:nt}=Q;!q&&y!==null&&(nt=this.playbackSpeed>=0?this.currentTime>=f:this.currentTime<=0);const F=this.holdTime===null&&(this.state==="finished"||this.state==="running"&&nt);return F&&N!==hc&&(Q.value=Pc(v,this.options,G,this.speed)),H&&H(Q.value),F&&this.finish(),Q}then(l,o){return this.finished.then(l,o)}get duration(){return Be(this.calculatedDuration)}get iterationDuration(){const{delay:l=0}=this.options||{};return this.duration+Be(l)}get time(){return Be(this.currentTime)}set time(l){l=yn(l),this.currentTime=l,this.startTime===null||this.holdTime!==null||this.playbackSpeed===0?this.holdTime=l:this.driver&&(this.startTime=this.driver.now()-l/this.playbackSpeed),this.driver?.start(!1)}get speed(){return this.playbackSpeed}set speed(l){this.updateTime(oe.now());const o=this.playbackSpeed!==l;this.playbackSpeed=l,o&&(this.time=Be(this.currentTime))}play(){if(this.isStopped)return;const{driver:l=$v,startTime:o}=this.options;this.driver||(this.driver=l(f=>this.tick(f))),this.options.onPlay?.();const r=this.driver.now();this.state==="finished"?(this.updateFinished(),this.startTime=r):this.holdTime!==null?this.startTime=r-this.holdTime:this.startTime||(this.startTime=o??r),this.state==="finished"&&this.speed<0&&(this.startTime+=this.calculatedDuration),this.holdTime=null,this.state="running",this.driver.start()}pause(){this.state="paused",this.updateTime(oe.now()),this.holdTime=this.currentTime}complete(){this.state!=="running"&&this.play(),this.state="finished",this.holdTime=null}finish(){this.notifyFinished(),this.teardown(),this.state="finished",this.options.onComplete?.()}cancel(){this.holdTime=null,this.startTime=0,this.tick(0),this.teardown(),this.options.onCancel?.()}teardown(){this.state="idle",this.stopDriver(),this.startTime=this.holdTime=null}stopDriver(){this.driver&&(this.driver.stop(),this.driver=void 0)}sample(l){return this.startTime=0,this.tick(l,!0)}attachTimeline(l){return this.options.allowFlatten&&(this.options.type="keyframes",this.options.ease="linear",this.initAnimation()),this.driver?.stop(),l.observe(this)}}function y2(i){for(let l=1;li*180/Math.PI,mc=i=>{const l=xi(Math.atan2(i[1],i[0]));return pc(l)},g2={x:4,y:5,translateX:4,translateY:5,scaleX:0,scaleY:3,scale:i=>(Math.abs(i[0])+Math.abs(i[3]))/2,rotate:mc,rotateZ:mc,skewX:i=>xi(Math.atan(i[1])),skewY:i=>xi(Math.atan(i[2])),skew:i=>(Math.abs(i[1])+Math.abs(i[2]))/2},pc=i=>(i=i%360,i<0&&(i+=360),i),Rp=mc,Op=i=>Math.sqrt(i[0]*i[0]+i[1]*i[1]),kp=i=>Math.sqrt(i[4]*i[4]+i[5]*i[5]),x2={x:12,y:13,z:14,translateX:12,translateY:13,translateZ:14,scaleX:Op,scaleY:kp,scale:i=>(Op(i)+kp(i))/2,rotateX:i=>pc(xi(Math.atan2(i[6],i[5]))),rotateY:i=>pc(xi(Math.atan2(-i[2],i[0]))),rotateZ:Rp,rotate:Rp,skewX:i=>xi(Math.atan(i[4])),skewY:i=>xi(Math.atan(i[1])),skew:i=>(Math.abs(i[1])+Math.abs(i[4]))/2};function yc(i){return i.includes("scale")?1:0}function gc(i,l){if(!i||i==="none")return yc(l);const o=i.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);let r,f;if(o)r=x2,f=o;else{const p=i.match(/^matrix\(([-\d.e\s,]+)\)$/u);r=g2,f=p}if(!f)return yc(l);const m=r[l],d=f[1].split(",").map(b2);return typeof m=="function"?m(d):d[m]}const v2=(i,l)=>{const{transform:o="none"}=getComputedStyle(i);return gc(o,l)};function b2(i){return parseFloat(i.trim())}const fa=["transformPerspective","x","y","z","translateX","translateY","translateZ","scale","scaleX","scaleY","rotate","rotateX","rotateY","rotateZ","skew","skewX","skewY"],da=new Set(fa),Vp=i=>i===ca||i===J,S2=new Set(["x","y","z"]),j2=fa.filter(i=>!S2.has(i));function T2(i){const l=[];return j2.forEach(o=>{const r=i.getValue(o);r!==void 0&&(l.push([o,r.get()]),r.set(o.startsWith("scale")?1:0))}),l}const Zn={width:({x:i},{paddingLeft:l="0",paddingRight:o="0"})=>i.max-i.min-parseFloat(l)-parseFloat(o),height:({y:i},{paddingTop:l="0",paddingBottom:o="0"})=>i.max-i.min-parseFloat(l)-parseFloat(o),top:(i,{top:l})=>parseFloat(l),left:(i,{left:l})=>parseFloat(l),bottom:({y:i},{top:l})=>parseFloat(l)+(i.max-i.min),right:({x:i},{left:l})=>parseFloat(l)+(i.max-i.min),x:(i,{transform:l})=>gc(l,"x"),y:(i,{transform:l})=>gc(l,"y")};Zn.translateX=Zn.x;Zn.translateY=Zn.y;const vi=new Set;let xc=!1,vc=!1,bc=!1;function w0(){if(vc){const i=Array.from(vi).filter(r=>r.needsMeasurement),l=new Set(i.map(r=>r.element)),o=new Map;l.forEach(r=>{const f=T2(r);f.length&&(o.set(r,f),r.render())}),i.forEach(r=>r.measureInitialState()),l.forEach(r=>{r.render();const f=o.get(r);f&&f.forEach(([m,d])=>{r.getValue(m)?.set(d)})}),i.forEach(r=>r.measureEndState()),i.forEach(r=>{r.suspendedScrollY!==void 0&&window.scrollTo(0,r.suspendedScrollY)})}vc=!1,xc=!1,vi.forEach(i=>i.complete(bc)),vi.clear()}function L0(){vi.forEach(i=>{i.readKeyframes(),i.needsMeasurement&&(vc=!0)})}function A2(){bc=!0,L0(),w0(),bc=!1}class tf{constructor(l,o,r,f,m,d=!1){this.state="pending",this.isAsync=!1,this.needsMeasurement=!1,this.unresolvedKeyframes=[...l],this.onComplete=o,this.name=r,this.motionValue=f,this.element=m,this.isAsync=d}scheduleResolve(){this.state="scheduled",this.isAsync?(vi.add(this),xc||(xc=!0,Dt.read(L0),Dt.resolveKeyframes(w0))):(this.readKeyframes(),this.complete())}readKeyframes(){const{unresolvedKeyframes:l,name:o,element:r,motionValue:f}=this;if(l[0]===null){const m=f?.get(),d=l[l.length-1];if(m!==void 0)l[0]=m;else if(r&&o){const p=r.readValue(o,d);p!=null&&(l[0]=p)}l[0]===void 0&&(l[0]=d),f&&m===void 0&&f.set(l[0])}y2(l)}setFinalKeyframe(){}measureInitialState(){}renderEndStyles(){}measureEndState(){}complete(l=!1){this.state="complete",this.onComplete(this.unresolvedKeyframes,this.finalKeyframe,l),vi.delete(this)}cancel(){this.state==="scheduled"&&(vi.delete(this),this.state="pending")}resume(){this.state==="pending"&&this.scheduleResolve()}}const C2=i=>i.startsWith("--");function M2(i,l,o){C2(l)?i.style.setProperty(l,o):i.style[l]=o}const E2=qc(()=>window.ScrollTimeline!==void 0),D2={};function w2(i,l){const o=qc(i);return()=>D2[l]??o()}const N0=w2(()=>{try{document.createElement("div").animate({opacity:0},{easing:"linear(0, 1)"})}catch{return!1}return!0},"linearEasing"),dl=([i,l,o,r])=>`cubic-bezier(${i}, ${l}, ${o}, ${r})`,Bp={linear:"linear",ease:"ease",easeIn:"ease-in",easeOut:"ease-out",easeInOut:"ease-in-out",circIn:dl([0,.65,.55,1]),circOut:dl([.55,0,1,.45]),backIn:dl([.31,.01,.66,-.59]),backOut:dl([.33,1.53,.69,.99])};function _0(i,l){if(i)return typeof i=="function"?N0()?M0(i,l):"ease-out":p0(i)?dl(i):Array.isArray(i)?i.map(o=>_0(o,l)||Bp.easeOut):Bp[i]}function L2(i,l,o,{delay:r=0,duration:f=300,repeat:m=0,repeatType:d="loop",ease:p="easeOut",times:y}={},g=void 0){const v={[l]:o};y&&(v.offset=y);const b=_0(p,f);Array.isArray(b)&&(v.easing=b);const T={delay:r,duration:f,easing:Array.isArray(b)?"linear":b,fill:"both",iterations:m+1,direction:d==="reverse"?"alternate":"normal"};return g&&(T.pseudoElement=g),i.animate(v,T)}function z0(i){return typeof i=="function"&&"applyToOptions"in i}function N2({type:i,...l}){return z0(i)&&N0()?i.applyToOptions(l):(l.duration??(l.duration=300),l.ease??(l.ease="easeOut"),l)}class _2 extends $c{constructor(l){if(super(),this.finishedTime=null,this.isStopped=!1,this.manualStartTime=null,!l)return;const{element:o,name:r,keyframes:f,pseudoElement:m,allowFlatten:d=!1,finalKeyframe:p,onComplete:y}=l;this.isPseudoElement=!!m,this.allowFlatten=d,this.options=l,Yc(typeof l.type!="string");const g=N2(l);this.animation=L2(o,r,f,g,m),g.autoplay===!1&&this.animation.pause(),this.animation.onfinish=()=>{if(this.finishedTime=this.time,!m){const v=Pc(f,this.options,p,this.speed);this.updateMotionValue?this.updateMotionValue(v):M2(o,r,v),this.animation.cancel()}y?.(),this.notifyFinished()}}play(){this.isStopped||(this.manualStartTime=null,this.animation.play(),this.state==="finished"&&this.updateFinished())}pause(){this.animation.pause()}complete(){this.animation.finish?.()}cancel(){try{this.animation.cancel()}catch{}}stop(){if(this.isStopped)return;this.isStopped=!0;const{state:l}=this;l==="idle"||l==="finished"||(this.updateMotionValue?this.updateMotionValue():this.commitStyles(),this.isPseudoElement||this.cancel())}commitStyles(){const l=this.options?.element;!this.isPseudoElement&&l?.isConnected&&this.animation.commitStyles?.()}get duration(){const l=this.animation.effect?.getComputedTiming?.().duration||0;return Be(Number(l))}get iterationDuration(){const{delay:l=0}=this.options||{};return this.duration+Be(l)}get time(){return Be(Number(this.animation.currentTime)||0)}set time(l){this.manualStartTime=null,this.finishedTime=null,this.animation.currentTime=yn(l)}get speed(){return this.animation.playbackRate}set speed(l){l<0&&(this.finishedTime=null),this.animation.playbackRate=l}get state(){return this.finishedTime!==null?"finished":this.animation.playState}get startTime(){return this.manualStartTime??Number(this.animation.startTime)}set startTime(l){this.manualStartTime=this.animation.startTime=l}attachTimeline({timeline:l,observe:o}){return this.allowFlatten&&this.animation.effect?.updateTiming({easing:"linear"}),this.animation.onfinish=null,l&&E2()?(this.animation.timeline=l,Ue):o(this)}}const R0={anticipate:f0,backInOut:c0,circInOut:h0};function z2(i){return i in R0}function R2(i){typeof i.ease=="string"&&z2(i.ease)&&(i.ease=R0[i.ease])}const Fu=10;class O2 extends _2{constructor(l){R2(l),D0(l),super(l),l.startTime!==void 0&&(this.startTime=l.startTime),this.options=l}updateMotionValue(l){const{motionValue:o,onUpdate:r,onComplete:f,element:m,...d}=this.options;if(!o)return;if(l!==void 0){o.set(l);return}const p=new Ic({...d,autoplay:!1}),y=Math.max(Fu,oe.now()-this.startTime),g=Fe(0,Fu,y-Fu);o.setWithVelocity(p.sample(Math.max(0,y-g)).value,p.sample(y).value,g),p.stop()}}const Up=(i,l)=>l==="zIndex"?!1:!!(typeof i=="number"||Array.isArray(i)||typeof i=="string"&&(Kn.test(i)||i==="0")&&!i.startsWith("url("));function k2(i){const l=i[0];if(i.length===1)return!0;for(let o=0;oObject.hasOwnProperty.call(Element.prototype,"animate"));function H2(i){const{motionValue:l,name:o,repeatDelay:r,repeatType:f,damping:m,type:d}=i;if(!(l?.owner?.current instanceof HTMLElement))return!1;const{onUpdate:y,transformTemplate:g}=l.owner.getProps();return U2()&&o&&B2.has(o)&&(o!=="transform"||!g)&&!y&&!r&&f!=="mirror"&&m!==0&&d!=="inertia"}const Y2=40;class q2 extends $c{constructor({autoplay:l=!0,delay:o=0,type:r="keyframes",repeat:f=0,repeatDelay:m=0,repeatType:d="loop",keyframes:p,name:y,motionValue:g,element:v,...b}){super(),this.stop=()=>{this._animation&&(this._animation.stop(),this.stopTimeline?.()),this.keyframeResolver?.cancel()},this.createdAt=oe.now();const T={autoplay:l,delay:o,type:r,repeat:f,repeatDelay:m,repeatType:d,name:y,motionValue:g,element:v,...b},w=v?.KeyframeResolver||tf;this.keyframeResolver=new w(p,(N,H,G)=>this.onKeyframesResolved(N,H,T,!G),y,g,v),this.keyframeResolver?.scheduleResolve()}onKeyframesResolved(l,o,r,f){this.keyframeResolver=void 0;const{name:m,type:d,velocity:p,delay:y,isHandoff:g,onUpdate:v}=r;this.resolvedAt=oe.now(),V2(l,m,d,p)||((gn.instantAnimations||!y)&&v?.(Pc(l,r,o)),l[0]=l[l.length-1],Sc(r),r.repeat=0);const T={startTime:f?this.resolvedAt?this.resolvedAt-this.createdAt>Y2?this.resolvedAt:this.createdAt:this.createdAt:void 0,finalKeyframe:o,...r,keyframes:l},w=!g&&H2(T),N=T.motionValue?.owner?.current,H=w?new O2({...T,element:N}):new Ic(T);H.finished.then(()=>{this.notifyFinished()}).catch(Ue),this.pendingTimeline&&(this.stopTimeline=H.attachTimeline(this.pendingTimeline),this.pendingTimeline=void 0),this._animation=H}get finished(){return this._animation?this.animation.finished:this._finished}then(l,o){return this.finished.finally(l).then(()=>{})}get animation(){return this._animation||(this.keyframeResolver?.resume(),A2()),this._animation}get duration(){return this.animation.duration}get iterationDuration(){return this.animation.iterationDuration}get time(){return this.animation.time}set time(l){this.animation.time=l}get speed(){return this.animation.speed}get state(){return this.animation.state}set speed(l){this.animation.speed=l}get startTime(){return this.animation.startTime}attachTimeline(l){return this._animation?this.stopTimeline=this.animation.attachTimeline(l):this.pendingTimeline=l,()=>this.stop()}play(){this.animation.play()}pause(){this.animation.pause()}complete(){this.animation.complete()}cancel(){this._animation&&this.animation.cancel(),this.keyframeResolver?.cancel()}}function O0(i,l,o,r=0,f=1){const m=Array.from(i).sort((g,v)=>g.sortNodePosition(v)).indexOf(l),d=i.size,p=(d-1)*r;return typeof o=="function"?o(m,d):f===1?m*r:p-m*r}const G2=/^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;function X2(i){const l=G2.exec(i);if(!l)return[,];const[,o,r,f]=l;return[`--${o??r}`,f]}function k0(i,l,o=1){const[r,f]=X2(i);if(!r)return;const m=window.getComputedStyle(l).getPropertyValue(r);if(m){const d=m.trim();return n0(d)?parseFloat(d):d}return Qc(f)?k0(f,l,o+1):f}const Z2={type:"spring",stiffness:500,damping:25,restSpeed:10},Q2=i=>({type:"spring",stiffness:550,damping:i===0?2*Math.sqrt(550):30,restSpeed:10}),K2={type:"keyframes",duration:.8},W2={type:"keyframes",ease:[.25,.1,.35,1],duration:.3},J2=(i,{keyframes:l})=>l.length>2?K2:da.has(i)?i.startsWith("scale")?Q2(l[1]):Z2:W2,F2=i=>i!==null;function P2(i,{repeat:l,repeatType:o="loop"},r){const f=i.filter(F2),m=l&&o!=="loop"&&l%2===1?0:f.length-1;return f[m]}function ef(i,l){return i?.[l]??i?.default??i}function $2({when:i,delay:l,delayChildren:o,staggerChildren:r,staggerDirection:f,repeat:m,repeatType:d,repeatDelay:p,from:y,elapsed:g,...v}){return!!Object.keys(v).length}const nf=(i,l,o,r={},f,m)=>d=>{const p=ef(r,i)||{},y=p.delay||r.delay||0;let{elapsed:g=0}=r;g=g-yn(y);const v={keyframes:Array.isArray(o)?o:[null,o],ease:"easeOut",velocity:l.getVelocity(),...p,delay:-g,onUpdate:T=>{l.set(T),p.onUpdate&&p.onUpdate(T)},onComplete:()=>{d(),p.onComplete&&p.onComplete()},name:i,motionValue:l,element:m?void 0:f};$2(p)||Object.assign(v,J2(i,v)),v.duration&&(v.duration=yn(v.duration)),v.repeatDelay&&(v.repeatDelay=yn(v.repeatDelay)),v.from!==void 0&&(v.keyframes[0]=v.from);let b=!1;if((v.type===!1||v.duration===0&&!v.repeatDelay)&&(Sc(v),v.delay===0&&(b=!0)),(gn.instantAnimations||gn.skipAnimations||f?.shouldSkipAnimations)&&(b=!0,Sc(v),v.delay=0),v.allowFlatten=!p.type&&!p.ease,b&&!m&&l.get()!==void 0){const T=P2(v.keyframes,p);if(T!==void 0){Dt.update(()=>{v.onUpdate(T),v.onComplete()});return}}return p.isSync?new Ic(v):new q2(v)};function Hp(i){const l=[{},{}];return i?.values.forEach((o,r)=>{l[0][r]=o.get(),l[1][r]=o.getVelocity()}),l}function af(i,l,o,r){if(typeof l=="function"){const[f,m]=Hp(r);l=l(o!==void 0?o:i.custom,f,m)}if(typeof l=="string"&&(l=i.variants&&i.variants[l]),typeof l=="function"){const[f,m]=Hp(r);l=l(o!==void 0?o:i.custom,f,m)}return l}function oa(i,l,o){const r=i.getProps();return af(r,l,o!==void 0?o:r.custom,i)}const V0=new Set(["width","height","top","left","right","bottom",...fa]),Yp=30,I2=i=>!isNaN(parseFloat(i));class tb{constructor(l,o={}){this.canTrackVelocity=null,this.events={},this.updateAndNotify=r=>{const f=oe.now();if(this.updatedAt!==f&&this.setPrevFrameValue(),this.prev=this.current,this.setCurrent(r),this.current!==this.prev&&(this.events.change?.notify(this.current),this.dependents))for(const m of this.dependents)m.dirty()},this.hasAnimated=!1,this.setCurrent(l),this.owner=o.owner}setCurrent(l){this.current=l,this.updatedAt=oe.now(),this.canTrackVelocity===null&&l!==void 0&&(this.canTrackVelocity=I2(this.current))}setPrevFrameValue(l=this.current){this.prevFrameValue=l,this.prevUpdatedAt=this.updatedAt}onChange(l){return this.on("change",l)}on(l,o){this.events[l]||(this.events[l]=new Gc);const r=this.events[l].add(o);return l==="change"?()=>{r(),Dt.read(()=>{this.events.change.getSize()||this.stop()})}:r}clearListeners(){for(const l in this.events)this.events[l].clear()}attach(l,o){this.passiveEffect=l,this.stopPassiveEffect=o}set(l){this.passiveEffect?this.passiveEffect(l,this.updateAndNotify):this.updateAndNotify(l)}setWithVelocity(l,o,r){this.set(o),this.prev=void 0,this.prevFrameValue=l,this.prevUpdatedAt=this.updatedAt-r}jump(l,o=!0){this.updateAndNotify(l),this.prev=l,this.prevUpdatedAt=this.prevFrameValue=void 0,o&&this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}dirty(){this.events.change?.notify(this.current)}addDependent(l){this.dependents||(this.dependents=new Set),this.dependents.add(l)}removeDependent(l){this.dependents&&this.dependents.delete(l)}get(){return this.current}getPrevious(){return this.prev}getVelocity(){const l=oe.now();if(!this.canTrackVelocity||this.prevFrameValue===void 0||l-this.updatedAt>Yp)return 0;const o=Math.min(this.updatedAt-this.prevUpdatedAt,Yp);return l0(parseFloat(this.current)-parseFloat(this.prevFrameValue),o)}start(l){return this.stop(),new Promise(o=>{this.hasAnimated=!0,this.animation=l(o),this.events.animationStart&&this.events.animationStart.notify()}).then(()=>{this.events.animationComplete&&this.events.animationComplete.notify(),this.clearAnimation()})}stop(){this.animation&&(this.animation.stop(),this.events.animationCancel&&this.events.animationCancel.notify()),this.clearAnimation()}isAnimating(){return!!this.animation}clearAnimation(){delete this.animation}destroy(){this.dependents?.clear(),this.events.destroy?.notify(),this.clearListeners(),this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}}function ua(i,l){return new tb(i,l)}const jc=i=>Array.isArray(i);function eb(i,l,o){i.hasValue(l)?i.getValue(l).set(o):i.addValue(l,ua(o))}function nb(i){return jc(i)?i[i.length-1]||0:i}function ib(i,l){const o=oa(i,l);let{transitionEnd:r={},transition:f={},...m}=o||{};m={...m,...r};for(const d in m){const p=nb(m[d]);eb(i,d,p)}}const le=i=>!!(i&&i.getVelocity);function ab(i){return!!(le(i)&&i.add)}function Tc(i,l){const o=i.getValue("willChange");if(ab(o))return o.add(l);if(!o&&gn.WillChange){const r=new gn.WillChange("auto");i.addValue("willChange",r),r.add(l)}}function lf(i){return i.replace(/([A-Z])/g,l=>`-${l.toLowerCase()}`)}const lb="framerAppearId",B0="data-"+lf(lb);function U0(i){return i.props[B0]}function sb({protectedKeys:i,needsAnimating:l},o){const r=i.hasOwnProperty(o)&&l[o]!==!0;return l[o]=!1,r}function H0(i,l,{delay:o=0,transitionOverride:r,type:f}={}){let{transition:m=i.getDefaultTransition(),transitionEnd:d,...p}=l;const y=m?.reduceMotion;r&&(m=r);const g=[],v=f&&i.animationState&&i.animationState.getState()[f];for(const b in p){const T=i.getValue(b,i.latestValues[b]??null),w=p[b];if(w===void 0||v&&sb(v,b))continue;const N={delay:o,...ef(m||{},b)},H=T.get();if(H!==void 0&&!T.isAnimating&&!Array.isArray(w)&&w===H&&!N.velocity)continue;let G=!1;if(window.MotionHandoffAnimation){const V=U0(i);if(V){const Z=window.MotionHandoffAnimation(V,b,Dt);Z!==null&&(N.startTime=Z,G=!0)}}Tc(i,b);const U=y??i.shouldReduceMotion;T.start(nf(b,T,w,U&&V0.has(b)?{type:!1}:N,i,G));const q=T.animation;q&&g.push(q)}return d&&Promise.all(g).then(()=>{Dt.update(()=>{d&&ib(i,d)})}),g}function Ac(i,l,o={}){const r=oa(i,l,o.type==="exit"?i.presenceContext?.custom:void 0);let{transition:f=i.getDefaultTransition()||{}}=r||{};o.transitionOverride&&(f=o.transitionOverride);const m=r?()=>Promise.all(H0(i,r,o)):()=>Promise.resolve(),d=i.variantChildren&&i.variantChildren.size?(y=0)=>{const{delayChildren:g=0,staggerChildren:v,staggerDirection:b}=f;return rb(i,l,y,g,v,b,o)}:()=>Promise.resolve(),{when:p}=f;if(p){const[y,g]=p==="beforeChildren"?[m,d]:[d,m];return y().then(()=>g())}else return Promise.all([m(),d(o.delay)])}function rb(i,l,o=0,r=0,f=0,m=1,d){const p=[];for(const y of i.variantChildren)y.notify("AnimationStart",l),p.push(Ac(y,l,{...d,delay:o+(typeof r=="function"?0:r)+O0(i.variantChildren,y,r,f,m)}).then(()=>y.notify("AnimationComplete",l)));return Promise.all(p)}function ob(i,l,o={}){i.notify("AnimationStart",l);let r;if(Array.isArray(l)){const f=l.map(m=>Ac(i,m,o));r=Promise.all(f)}else if(typeof l=="string")r=Ac(i,l,o);else{const f=typeof l=="function"?oa(i,l,o.custom):l;r=Promise.all(H0(i,f,o))}return r.then(()=>{i.notify("AnimationComplete",l)})}const ub={test:i=>i==="auto",parse:i=>i},Y0=i=>l=>l.test(i),q0=[ca,J,Je,Gn,kv,Ov,ub],qp=i=>q0.find(Y0(i));function cb(i){return typeof i=="number"?i===0:i!==null?i==="none"||i==="0"||a0(i):!0}const fb=new Set(["brightness","contrast","saturate","opacity"]);function db(i){const[l,o]=i.slice(0,-1).split("(");if(l==="drop-shadow")return i;const[r]=o.match(Kc)||[];if(!r)return i;const f=o.replace(r,"");let m=fb.has(l)?1:0;return r!==o&&(m*=100),l+"("+m+f+")"}const hb=/\b([a-z-]*)\(.*?\)/gu,Cc={...Kn,getAnimatableNone:i=>{const l=i.match(hb);return l?l.map(db).join(" "):i}},Gp={...ca,transform:Math.round},mb={rotate:Gn,rotateX:Gn,rotateY:Gn,rotateZ:Gn,scale:Ks,scaleX:Ks,scaleY:Ks,scaleZ:Ks,skew:Gn,skewX:Gn,skewY:Gn,distance:J,translateX:J,translateY:J,translateZ:J,x:J,y:J,z:J,perspective:J,transformPerspective:J,opacity:bl,originX:wp,originY:wp,originZ:J},sf={borderWidth:J,borderTopWidth:J,borderRightWidth:J,borderBottomWidth:J,borderLeftWidth:J,borderRadius:J,borderTopLeftRadius:J,borderTopRightRadius:J,borderBottomRightRadius:J,borderBottomLeftRadius:J,width:J,maxWidth:J,height:J,maxHeight:J,top:J,right:J,bottom:J,left:J,inset:J,insetBlock:J,insetBlockStart:J,insetBlockEnd:J,insetInline:J,insetInlineStart:J,insetInlineEnd:J,padding:J,paddingTop:J,paddingRight:J,paddingBottom:J,paddingLeft:J,paddingBlock:J,paddingBlockStart:J,paddingBlockEnd:J,paddingInline:J,paddingInlineStart:J,paddingInlineEnd:J,margin:J,marginTop:J,marginRight:J,marginBottom:J,marginLeft:J,marginBlock:J,marginBlockStart:J,marginBlockEnd:J,marginInline:J,marginInlineStart:J,marginInlineEnd:J,fontSize:J,backgroundPositionX:J,backgroundPositionY:J,...mb,zIndex:Gp,fillOpacity:bl,strokeOpacity:bl,numOctaves:Gp},pb={...sf,color:Zt,backgroundColor:Zt,outlineColor:Zt,fill:Zt,stroke:Zt,borderColor:Zt,borderTopColor:Zt,borderRightColor:Zt,borderBottomColor:Zt,borderLeftColor:Zt,filter:Cc,WebkitFilter:Cc},G0=i=>pb[i];function X0(i,l){let o=G0(i);return o!==Cc&&(o=Kn),o.getAnimatableNone?o.getAnimatableNone(l):void 0}const yb=new Set(["auto","none","0"]);function gb(i,l,o){let r=0,f;for(;r{l.getValue(p).set(y)}),this.resolveNoneKeyframes()}}function vb(i,l,o){if(i==null)return[];if(i instanceof EventTarget)return[i];if(typeof i=="string"){let r=document;const f=o?.[i]??r.querySelectorAll(i);return f?Array.from(f):[]}return Array.from(i).filter(r=>r!=null)}const Z0=(i,l)=>l&&typeof i=="number"?l.transform(i):i;function Mc(i){return i0(i)&&"offsetHeight"in i}const{schedule:rf}=y0(queueMicrotask,!1),Xe={x:!1,y:!1};function Q0(){return Xe.x||Xe.y}function bb(i){return i==="x"||i==="y"?Xe[i]?null:(Xe[i]=!0,()=>{Xe[i]=!1}):Xe.x||Xe.y?null:(Xe.x=Xe.y=!0,()=>{Xe.x=Xe.y=!1})}function K0(i,l){const o=vb(i),r=new AbortController,f={passive:!0,...l,signal:r.signal};return[o,f,()=>r.abort()]}function Xp(i){return!(i.pointerType==="touch"||Q0())}function Sb(i,l,o={}){const[r,f,m]=K0(i,o),d=p=>{if(!Xp(p))return;const{target:y}=p,g=l(y,p);if(typeof g!="function"||!y)return;const v=b=>{Xp(b)&&(g(b),y.removeEventListener("pointerleave",v))};y.addEventListener("pointerleave",v,f)};return r.forEach(p=>{p.addEventListener("pointerenter",d,f)}),m}const W0=(i,l)=>l?i===l?!0:W0(i,l.parentElement):!1,of=i=>i.pointerType==="mouse"?typeof i.button!="number"||i.button<=0:i.isPrimary!==!1,jb=new Set(["BUTTON","INPUT","SELECT","TEXTAREA","A"]);function Tb(i){return jb.has(i.tagName)||i.isContentEditable===!0}const Ab=new Set(["INPUT","SELECT","TEXTAREA"]);function Cb(i){return Ab.has(i.tagName)||i.isContentEditable===!0}const lr=new WeakSet;function Zp(i){return l=>{l.key==="Enter"&&i(l)}}function Pu(i,l){i.dispatchEvent(new PointerEvent("pointer"+l,{isPrimary:!0,bubbles:!0}))}const Mb=(i,l)=>{const o=i.currentTarget;if(!o)return;const r=Zp(()=>{if(lr.has(o))return;Pu(o,"down");const f=Zp(()=>{Pu(o,"up")}),m=()=>Pu(o,"cancel");o.addEventListener("keyup",f,l),o.addEventListener("blur",m,l)});o.addEventListener("keydown",r,l),o.addEventListener("blur",()=>o.removeEventListener("keydown",r),l)};function Qp(i){return of(i)&&!Q0()}function Eb(i,l,o={}){const[r,f,m]=K0(i,o),d=p=>{const y=p.currentTarget;if(!Qp(p))return;lr.add(y);const g=l(y,p),v=(w,N)=>{window.removeEventListener("pointerup",b),window.removeEventListener("pointercancel",T),lr.has(y)&&lr.delete(y),Qp(w)&&typeof g=="function"&&g(w,{success:N})},b=w=>{v(w,y===window||y===document||o.useGlobalTarget||W0(y,w.target))},T=w=>{v(w,!1)};window.addEventListener("pointerup",b,f),window.addEventListener("pointercancel",T,f)};return r.forEach(p=>{(o.useGlobalTarget?window:p).addEventListener("pointerdown",d,f),Mc(p)&&(p.addEventListener("focus",g=>Mb(g,f)),!Tb(p)&&!p.hasAttribute("tabindex")&&(p.tabIndex=0))}),m}function J0(i){return i0(i)&&"ownerSVGElement"in i}function Db(i){return J0(i)&&i.tagName==="svg"}const wb=[...q0,Zt,Kn],Lb=i=>wb.find(Y0(i)),Kp=()=>({translate:0,scale:1,origin:0,originPoint:0}),la=()=>({x:Kp(),y:Kp()}),Wp=()=>({min:0,max:0}),Wt=()=>({x:Wp(),y:Wp()}),Ec={current:null},F0={current:!1},Nb=typeof window<"u";function _b(){if(F0.current=!0,!!Nb)if(window.matchMedia){const i=window.matchMedia("(prefers-reduced-motion)"),l=()=>Ec.current=i.matches;i.addEventListener("change",l),l()}else Ec.current=!1}const zb=new WeakMap;function yr(i){return i!==null&&typeof i=="object"&&typeof i.start=="function"}function jl(i){return typeof i=="string"||Array.isArray(i)}const uf=["animate","whileInView","whileFocus","whileHover","whileTap","whileDrag","exit"],cf=["initial",...uf];function gr(i){return yr(i.animate)||cf.some(l=>jl(i[l]))}function P0(i){return!!(gr(i)||i.variants)}function Rb(i,l,o){for(const r in l){const f=l[r],m=o[r];if(le(f))i.addValue(r,f);else if(le(m))i.addValue(r,ua(f,{owner:i}));else if(m!==f)if(i.hasValue(r)){const d=i.getValue(r);d.liveStyle===!0?d.jump(f):d.hasAnimated||d.set(f)}else{const d=i.getStaticValue(r);i.addValue(r,ua(d!==void 0?d:f,{owner:i}))}}for(const r in o)l[r]===void 0&&i.removeValue(r);return l}const Jp=["AnimationStart","AnimationComplete","Update","BeforeLayoutMeasure","LayoutMeasure","LayoutAnimationStart","LayoutAnimationComplete"];let fr={};function $0(i){fr=i}function Ob(){return fr}class kb{scrapeMotionValuesFromProps(l,o,r){return{}}constructor({parent:l,props:o,presenceContext:r,reducedMotionConfig:f,skipAnimations:m,blockInitialAnimation:d,visualState:p},y={}){this.current=null,this.children=new Set,this.isVariantNode=!1,this.isControllingVariants=!1,this.shouldReduceMotion=null,this.shouldSkipAnimations=!1,this.values=new Map,this.KeyframeResolver=tf,this.features={},this.valueSubscriptions=new Map,this.prevMotionValues={},this.events={},this.propEventSubscriptions={},this.notifyUpdate=()=>this.notify("Update",this.latestValues),this.render=()=>{this.current&&(this.triggerBuild(),this.renderInstance(this.current,this.renderState,this.props.style,this.projection))},this.renderScheduledAt=0,this.scheduleRender=()=>{const w=oe.now();this.renderScheduledAtthis.bindToMotionValue(r,o)),this.reducedMotionConfig==="never"?this.shouldReduceMotion=!1:this.reducedMotionConfig==="always"?this.shouldReduceMotion=!0:(F0.current||_b(),this.shouldReduceMotion=Ec.current),this.shouldSkipAnimations=this.skipAnimationsConfig??!1,this.parent?.addChild(this),this.update(this.props,this.presenceContext)}unmount(){this.projection&&this.projection.unmount(),Qn(this.notifyUpdate),Qn(this.render),this.valueSubscriptions.forEach(l=>l()),this.valueSubscriptions.clear(),this.removeFromVariantTree&&this.removeFromVariantTree(),this.parent?.removeChild(this);for(const l in this.events)this.events[l].clear();for(const l in this.features){const o=this.features[l];o&&(o.unmount(),o.isMounted=!1)}this.current=null}addChild(l){this.children.add(l),this.enteringChildren??(this.enteringChildren=new Set),this.enteringChildren.add(l)}removeChild(l){this.children.delete(l),this.enteringChildren&&this.enteringChildren.delete(l)}bindToMotionValue(l,o){this.valueSubscriptions.has(l)&&this.valueSubscriptions.get(l)();const r=da.has(l);r&&this.onBindTransform&&this.onBindTransform();const f=o.on("change",d=>{this.latestValues[l]=d,this.props.onUpdate&&Dt.preRender(this.notifyUpdate),r&&this.projection&&(this.projection.isTransformDirty=!0),this.scheduleRender()});let m;typeof window<"u"&&window.MotionCheckAppearSync&&(m=window.MotionCheckAppearSync(this,l,o)),this.valueSubscriptions.set(l,()=>{f(),m&&m(),o.owner&&o.stop()})}sortNodePosition(l){return!this.current||!this.sortInstanceNodePosition||this.type!==l.type?0:this.sortInstanceNodePosition(this.current,l.current)}updateFeatures(){let l="animation";for(l in fr){const o=fr[l];if(!o)continue;const{isEnabled:r,Feature:f}=o;if(!this.features[l]&&f&&r(this.props)&&(this.features[l]=new f(this)),this.features[l]){const m=this.features[l];m.isMounted?m.update():(m.mount(),m.isMounted=!0)}}}triggerBuild(){this.build(this.renderState,this.latestValues,this.props)}measureViewportBox(){return this.current?this.measureInstanceViewportBox(this.current,this.props):Wt()}getStaticValue(l){return this.latestValues[l]}setStaticValue(l,o){this.latestValues[l]=o}update(l,o){(l.transformTemplate||this.props.transformTemplate)&&this.scheduleRender(),this.prevProps=this.props,this.props=l,this.prevPresenceContext=this.presenceContext,this.presenceContext=o;for(let r=0;ro.variantChildren.delete(l)}addValue(l,o){const r=this.values.get(l);o!==r&&(r&&this.removeValue(l),this.bindToMotionValue(l,o),this.values.set(l,o),this.latestValues[l]=o.get())}removeValue(l){this.values.delete(l);const o=this.valueSubscriptions.get(l);o&&(o(),this.valueSubscriptions.delete(l)),delete this.latestValues[l],this.removeValueFromRenderState(l,this.renderState)}hasValue(l){return this.values.has(l)}getValue(l,o){if(this.props.values&&this.props.values[l])return this.props.values[l];let r=this.values.get(l);return r===void 0&&o!==void 0&&(r=ua(o===null?void 0:o,{owner:this}),this.addValue(l,r)),r}readValue(l,o){let r=this.latestValues[l]!==void 0||!this.current?this.latestValues[l]:this.getBaseTargetFromProps(this.props,l)??this.readValueFromInstance(this.current,l,this.options);return r!=null&&(typeof r=="string"&&(n0(r)||a0(r))?r=parseFloat(r):!Lb(r)&&Kn.test(o)&&(r=X0(l,o)),this.setBaseTarget(l,le(r)?r.get():r)),le(r)?r.get():r}setBaseTarget(l,o){this.baseTarget[l]=o}getBaseTarget(l){const{initial:o}=this.props;let r;if(typeof o=="string"||typeof o=="object"){const m=af(this.props,o,this.presenceContext?.custom);m&&(r=m[l])}if(o&&r!==void 0)return r;const f=this.getBaseTargetFromProps(this.props,l);return f!==void 0&&!le(f)?f:this.initialValues[l]!==void 0&&r===void 0?void 0:this.baseTarget[l]}on(l,o){return this.events[l]||(this.events[l]=new Gc),this.events[l].add(o)}notify(l,...o){this.events[l]&&this.events[l].notify(...o)}scheduleRenderMicrotask(){rf.render(this.render)}}class I0 extends kb{constructor(){super(...arguments),this.KeyframeResolver=xb}sortInstanceNodePosition(l,o){return l.compareDocumentPosition(o)&2?1:-1}getBaseTargetFromProps(l,o){const r=l.style;return r?r[o]:void 0}removeValueFromRenderState(l,{vars:o,style:r}){delete o[l],delete r[l]}handleChildMotionValue(){this.childSubscription&&(this.childSubscription(),delete this.childSubscription);const{children:l}=this.props;le(l)&&(this.childSubscription=l.on("change",o=>{this.current&&(this.current.textContent=`${o}`)}))}}class Wn{constructor(l){this.isMounted=!1,this.node=l}update(){}}function tg({top:i,left:l,right:o,bottom:r}){return{x:{min:l,max:o},y:{min:i,max:r}}}function Vb({x:i,y:l}){return{top:l.min,right:i.max,bottom:l.max,left:i.min}}function Bb(i,l){if(!l)return i;const o=l({x:i.left,y:i.top}),r=l({x:i.right,y:i.bottom});return{top:o.y,left:o.x,bottom:r.y,right:r.x}}function $u(i){return i===void 0||i===1}function Dc({scale:i,scaleX:l,scaleY:o}){return!$u(i)||!$u(l)||!$u(o)}function yi(i){return Dc(i)||eg(i)||i.z||i.rotate||i.rotateX||i.rotateY||i.skewX||i.skewY}function eg(i){return Fp(i.x)||Fp(i.y)}function Fp(i){return i&&i!=="0%"}function dr(i,l,o){const r=i-o,f=l*r;return o+f}function Pp(i,l,o,r,f){return f!==void 0&&(i=dr(i,f,r)),dr(i,o,r)+l}function wc(i,l=0,o=1,r,f){i.min=Pp(i.min,l,o,r,f),i.max=Pp(i.max,l,o,r,f)}function ng(i,{x:l,y:o}){wc(i.x,l.translate,l.scale,l.originPoint),wc(i.y,o.translate,o.scale,o.originPoint)}const $p=.999999999999,Ip=1.0000000000001;function Ub(i,l,o,r=!1){const f=o.length;if(!f)return;l.x=l.y=1;let m,d;for(let p=0;p$p&&(l.x=1),l.y$p&&(l.y=1)}function sa(i,l){i.min=i.min+l,i.max=i.max+l}function ty(i,l,o,r,f=.5){const m=zt(i.min,i.max,f);wc(i,l,o,m,r)}function ra(i,l){ty(i.x,l.x,l.scaleX,l.scale,l.originX),ty(i.y,l.y,l.scaleY,l.scale,l.originY)}function ig(i,l){return tg(Bb(i.getBoundingClientRect(),l))}function Hb(i,l,o){const r=ig(i,o),{scroll:f}=l;return f&&(sa(r.x,f.offset.x),sa(r.y,f.offset.y)),r}const Yb={x:"translateX",y:"translateY",z:"translateZ",transformPerspective:"perspective"},qb=fa.length;function Gb(i,l,o){let r="",f=!0;for(let m=0;m{if(!l.target)return i;if(typeof i=="string")if(J.test(i))i=parseFloat(i);else return i;const o=ey(i,l.target.x),r=ey(i,l.target.y);return`${o}% ${r}%`}},Xb={correct:(i,{treeScale:l,projectionDelta:o})=>{const r=i,f=Kn.parse(i);if(f.length>5)return r;const m=Kn.createTransformer(i),d=typeof f[0]!="number"?1:0,p=o.x.scale*l.x,y=o.y.scale*l.y;f[0+d]/=p,f[1+d]/=y;const g=zt(p,y,.5);return typeof f[2+d]=="number"&&(f[2+d]/=g),typeof f[3+d]=="number"&&(f[3+d]/=g),m(f)}},Lc={borderRadius:{...ol,applyTo:["borderTopLeftRadius","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius"]},borderTopLeftRadius:ol,borderTopRightRadius:ol,borderBottomLeftRadius:ol,borderBottomRightRadius:ol,boxShadow:Xb};function lg(i,{layout:l,layoutId:o}){return da.has(i)||i.startsWith("origin")||(l||o!==void 0)&&(!!Lc[i]||i==="opacity")}function df(i,l,o){const r=i.style,f=l?.style,m={};if(!r)return m;for(const d in r)(le(r[d])||f&&le(f[d])||lg(d,i)||o?.getValue(d)?.liveStyle!==void 0)&&(m[d]=r[d]);return m}function Zb(i){return window.getComputedStyle(i)}class Qb extends I0{constructor(){super(...arguments),this.type="html",this.renderInstance=ag}readValueFromInstance(l,o){if(da.has(o))return this.projection?.isProjecting?yc(o):v2(l,o);{const r=Zb(l),f=(x0(o)?r.getPropertyValue(o):r[o])||0;return typeof f=="string"?f.trim():f}}measureInstanceViewportBox(l,{transformPagePoint:o}){return ig(l,o)}build(l,o,r){ff(l,o,r.transformTemplate)}scrapeMotionValuesFromProps(l,o,r){return df(l,o,r)}}const Kb={offset:"stroke-dashoffset",array:"stroke-dasharray"},Wb={offset:"strokeDashoffset",array:"strokeDasharray"};function Jb(i,l,o=1,r=0,f=!0){i.pathLength=1;const m=f?Kb:Wb;i[m.offset]=`${-r}`,i[m.array]=`${l} ${o}`}const Fb=["offsetDistance","offsetPath","offsetRotate","offsetAnchor"];function sg(i,{attrX:l,attrY:o,attrScale:r,pathLength:f,pathSpacing:m=1,pathOffset:d=0,...p},y,g,v){if(ff(i,p,g),y){i.style.viewBox&&(i.attrs.viewBox=i.style.viewBox);return}i.attrs=i.style,i.style={};const{attrs:b,style:T}=i;b.transform&&(T.transform=b.transform,delete b.transform),(T.transform||b.transformOrigin)&&(T.transformOrigin=b.transformOrigin??"50% 50%",delete b.transformOrigin),T.transform&&(T.transformBox=v?.transformBox??"fill-box",delete b.transformBox);for(const w of Fb)b[w]!==void 0&&(T[w]=b[w],delete b[w]);l!==void 0&&(b.x=l),o!==void 0&&(b.y=o),r!==void 0&&(b.scale=r),f!==void 0&&Jb(b,f,m,d,!1)}const rg=new Set(["baseFrequency","diffuseConstant","kernelMatrix","kernelUnitLength","keySplines","keyTimes","limitingConeAngle","markerHeight","markerWidth","numOctaves","targetX","targetY","surfaceScale","specularConstant","specularExponent","stdDeviation","tableValues","viewBox","gradientTransform","pathLength","startOffset","textLength","lengthAdjust"]),og=i=>typeof i=="string"&&i.toLowerCase()==="svg";function Pb(i,l,o,r){ag(i,l,void 0,r);for(const f in l.attrs)i.setAttribute(rg.has(f)?f:lf(f),l.attrs[f])}function ug(i,l,o){const r=df(i,l,o);for(const f in i)if(le(i[f])||le(l[f])){const m=fa.indexOf(f)!==-1?"attr"+f.charAt(0).toUpperCase()+f.substring(1):f;r[m]=i[f]}return r}class $b extends I0{constructor(){super(...arguments),this.type="svg",this.isSVGTag=!1,this.measureInstanceViewportBox=Wt}getBaseTargetFromProps(l,o){return l[o]}readValueFromInstance(l,o){if(da.has(o)){const r=G0(o);return r&&r.default||0}return o=rg.has(o)?o:lf(o),l.getAttribute(o)}scrapeMotionValuesFromProps(l,o,r){return ug(l,o,r)}build(l,o,r){sg(l,o,this.isSVGTag,r.transformTemplate,r.style)}renderInstance(l,o,r,f){Pb(l,o,r,f)}mount(l){this.isSVGTag=og(l.tagName),super.mount(l)}}const Ib=cf.length;function cg(i){if(!i)return;if(!i.isControllingVariants){const o=i.parent?cg(i.parent)||{}:{};return i.props.initial!==void 0&&(o.initial=i.props.initial),o}const l={};for(let o=0;oPromise.all(l.map(({animation:o,options:r})=>ob(i,o,r)))}function iS(i){let l=nS(i),o=ny(),r=!0;const f=y=>(g,v)=>{const b=oa(i,v,y==="exit"?i.presenceContext?.custom:void 0);if(b){const{transition:T,transitionEnd:w,...N}=b;g={...g,...N,...w}}return g};function m(y){l=y(i)}function d(y){const{props:g}=i,v=cg(i.parent)||{},b=[],T=new Set;let w={},N=1/0;for(let G=0;GN&&Z,it=!1;const yt=Array.isArray(V)?V:[V];let gt=yt.reduce(f(U),{});Q===!1&&(gt={});const{prevResolvedValues:Nt={}}=q,Jt={...Nt,...gt},Ht=B=>{K=!0,T.has(B)&&(it=!0,T.delete(B)),q.needsAnimating[B]=!0;const P=i.getValue(B);P&&(P.liveStyle=!1)};for(const B in Jt){const P=gt[B],ot=Nt[B];if(w.hasOwnProperty(B))continue;let dt=!1;jc(P)&&jc(ot)?dt=!fg(P,ot):dt=P!==ot,dt?P!=null?Ht(B):T.add(B):P!==void 0&&T.has(B)?Ht(B):q.protectedKeys[B]=!0}q.prevProp=V,q.prevResolvedValues=gt,q.isActive&&(w={...w,...gt}),r&&i.blockInitialAnimation&&(K=!1);const Qt=nt&&F;K&&(!Qt||it)&&b.push(...yt.map(B=>{const P={type:U};if(typeof B=="string"&&r&&!Qt&&i.manuallyAnimateOnMount&&i.parent){const{parent:ot}=i,dt=oa(ot,B);if(ot.enteringChildren&&dt){const{delayChildren:A}=dt.transition||{};P.delay=O0(ot.enteringChildren,i,A)}}return{animation:B,options:P}}))}if(T.size){const G={};if(typeof g.initial!="boolean"){const U=oa(i,Array.isArray(g.initial)?g.initial[0]:g.initial);U&&U.transition&&(G.transition=U.transition)}T.forEach(U=>{const q=i.getBaseTarget(U),V=i.getValue(U);V&&(V.liveStyle=!0),G[U]=q??null}),b.push({animation:G})}let H=!!b.length;return r&&(g.initial===!1||g.initial===g.animate)&&!i.manuallyAnimateOnMount&&(H=!1),r=!1,H?l(b):Promise.resolve()}function p(y,g){if(o[y].isActive===g)return Promise.resolve();i.variantChildren?.forEach(b=>b.animationState?.setActive(y,g)),o[y].isActive=g;const v=d(y);for(const b in o)o[b].protectedKeys={};return v}return{animateChanges:d,setActive:p,setAnimateFunction:m,getState:()=>o,reset:()=>{o=ny()}}}function aS(i,l){return typeof l=="string"?l!==i:Array.isArray(l)?!fg(l,i):!1}function pi(i=!1){return{isActive:i,protectedKeys:{},needsAnimating:{},prevResolvedValues:{}}}function ny(){return{animate:pi(!0),whileInView:pi(),whileHover:pi(),whileTap:pi(),whileDrag:pi(),whileFocus:pi(),exit:pi()}}function iy(i,l){i.min=l.min,i.max=l.max}function Ge(i,l){iy(i.x,l.x),iy(i.y,l.y)}function ay(i,l){i.translate=l.translate,i.scale=l.scale,i.originPoint=l.originPoint,i.origin=l.origin}const dg=1e-4,lS=1-dg,sS=1+dg,hg=.01,rS=0-hg,oS=0+hg;function ue(i){return i.max-i.min}function uS(i,l,o){return Math.abs(i-l)<=o}function ly(i,l,o,r=.5){i.origin=r,i.originPoint=zt(l.min,l.max,i.origin),i.scale=ue(o)/ue(l),i.translate=zt(o.min,o.max,i.origin)-i.originPoint,(i.scale>=lS&&i.scale<=sS||isNaN(i.scale))&&(i.scale=1),(i.translate>=rS&&i.translate<=oS||isNaN(i.translate))&&(i.translate=0)}function yl(i,l,o,r){ly(i.x,l.x,o.x,r?r.originX:void 0),ly(i.y,l.y,o.y,r?r.originY:void 0)}function sy(i,l,o){i.min=o.min+l.min,i.max=i.min+ue(l)}function cS(i,l,o){sy(i.x,l.x,o.x),sy(i.y,l.y,o.y)}function ry(i,l,o){i.min=l.min-o.min,i.max=i.min+ue(l)}function hr(i,l,o){ry(i.x,l.x,o.x),ry(i.y,l.y,o.y)}function oy(i,l,o,r,f){return i-=l,i=dr(i,1/o,r),f!==void 0&&(i=dr(i,1/f,r)),i}function fS(i,l=0,o=1,r=.5,f,m=i,d=i){if(Je.test(l)&&(l=parseFloat(l),l=zt(d.min,d.max,l/100)-d.min),typeof l!="number")return;let p=zt(m.min,m.max,r);i===m&&(p-=l),i.min=oy(i.min,l,o,p,f),i.max=oy(i.max,l,o,p,f)}function uy(i,l,[o,r,f],m,d){fS(i,l[o],l[r],l[f],l.scale,m,d)}const dS=["x","scaleX","originX"],hS=["y","scaleY","originY"];function cy(i,l,o,r){uy(i.x,l,dS,o?o.x:void 0,r?r.x:void 0),uy(i.y,l,hS,o?o.y:void 0,r?r.y:void 0)}function fy(i){return i.translate===0&&i.scale===1}function mg(i){return fy(i.x)&&fy(i.y)}function dy(i,l){return i.min===l.min&&i.max===l.max}function mS(i,l){return dy(i.x,l.x)&&dy(i.y,l.y)}function hy(i,l){return Math.round(i.min)===Math.round(l.min)&&Math.round(i.max)===Math.round(l.max)}function pg(i,l){return hy(i.x,l.x)&&hy(i.y,l.y)}function my(i){return ue(i.x)/ue(i.y)}function py(i,l){return i.translate===l.translate&&i.scale===l.scale&&i.originPoint===l.originPoint}function Ve(i){return[i("x"),i("y")]}function pS(i,l,o){let r="";const f=i.x.translate/l.x,m=i.y.translate/l.y,d=o?.z||0;if((f||m||d)&&(r=`translate3d(${f}px, ${m}px, ${d}px) `),(l.x!==1||l.y!==1)&&(r+=`scale(${1/l.x}, ${1/l.y}) `),o){const{transformPerspective:g,rotate:v,rotateX:b,rotateY:T,skewX:w,skewY:N}=o;g&&(r=`perspective(${g}px) ${r}`),v&&(r+=`rotate(${v}deg) `),b&&(r+=`rotateX(${b}deg) `),T&&(r+=`rotateY(${T}deg) `),w&&(r+=`skewX(${w}deg) `),N&&(r+=`skewY(${N}deg) `)}const p=i.x.scale*l.x,y=i.y.scale*l.y;return(p!==1||y!==1)&&(r+=`scale(${p}, ${y})`),r||"none"}const yg=["TopLeft","TopRight","BottomLeft","BottomRight"],yS=yg.length,yy=i=>typeof i=="string"?parseFloat(i):i,gy=i=>typeof i=="number"||J.test(i);function gS(i,l,o,r,f,m){f?(i.opacity=zt(0,o.opacity??1,xS(r)),i.opacityExit=zt(l.opacity??1,0,vS(r))):m&&(i.opacity=zt(l.opacity??1,o.opacity??1,r));for(let d=0;drl?1:o(vl(i,l,r))}function bS(i,l,o){const r=le(i)?i:ua(i);return r.start(nf("",r,l,o)),r.animation}function Tl(i,l,o,r={passive:!0}){return i.addEventListener(l,o,r),()=>i.removeEventListener(l,o)}const SS=(i,l)=>i.depth-l.depth;class jS{constructor(){this.children=[],this.isDirty=!1}add(l){Uc(this.children,l),this.isDirty=!0}remove(l){Hc(this.children,l),this.isDirty=!0}forEach(l){this.isDirty&&this.children.sort(SS),this.isDirty=!1,this.children.forEach(l)}}function TS(i,l){const o=oe.now(),r=({timestamp:f})=>{const m=f-o;m>=l&&(Qn(r),i(m-l))};return Dt.setup(r,!0),()=>Qn(r)}function sr(i){return le(i)?i.get():i}class AS{constructor(){this.members=[]}add(l){Uc(this.members,l),l.scheduleRender()}remove(l){if(Hc(this.members,l),l===this.prevLead&&(this.prevLead=void 0),l===this.lead){const o=this.members[this.members.length-1];o&&this.promote(o)}}relegate(l){const o=this.members.findIndex(f=>l===f);if(o===0)return!1;let r;for(let f=o;f>=0;f--){const m=this.members[f];if(m.isPresent!==!1){r=m;break}}return r?(this.promote(r),!0):!1}promote(l,o){const r=this.lead;if(l!==r&&(this.prevLead=r,this.lead=l,l.show(),r)){r.instance&&r.scheduleRender(),l.scheduleRender();const f=r.options.layoutDependency,m=l.options.layoutDependency;f!==void 0&&m!==void 0&&f===m||(l.resumeFrom=r,o&&(l.resumeFrom.preserveOpacity=!0),r.snapshot&&(l.snapshot=r.snapshot,l.snapshot.latestValues=r.animationValues||r.latestValues),l.root&&l.root.isUpdating&&(l.isLayoutDirty=!0));const{crossfade:p}=l.options;p===!1&&r.hide()}}exitAnimationComplete(){this.members.forEach(l=>{const{options:o,resumingFrom:r}=l;o.onExitComplete&&o.onExitComplete(),r&&r.options.onExitComplete&&r.options.onExitComplete()})}scheduleRender(){this.members.forEach(l=>{l.instance&&l.scheduleRender(!1)})}removeLeadSnapshot(){this.lead&&this.lead.snapshot&&(this.lead.snapshot=void 0)}}const rr={hasAnimatedSinceResize:!0,hasEverUpdated:!1},Iu=["","X","Y","Z"],CS=1e3;let MS=0;function tc(i,l,o,r){const{latestValues:f}=l;f[i]&&(o[i]=f[i],l.setStaticValue(i,0),r&&(r[i]=0))}function xg(i){if(i.hasCheckedOptimisedAppear=!0,i.root===i)return;const{visualElement:l}=i.options;if(!l)return;const o=U0(l);if(window.MotionHasOptimisedAnimation(o,"transform")){const{layout:f,layoutId:m}=i.options;window.MotionCancelOptimisedAnimation(o,"transform",Dt,!(f||m))}const{parent:r}=i;r&&!r.hasCheckedOptimisedAppear&&xg(r)}function vg({attachResizeListener:i,defaultParent:l,measureScroll:o,checkIsScrollRoot:r,resetTransform:f}){return class{constructor(d={},p=l?.()){this.id=MS++,this.animationId=0,this.animationCommitId=0,this.children=new Set,this.options={},this.isTreeAnimating=!1,this.isAnimationBlocked=!1,this.isLayoutDirty=!1,this.isProjectionDirty=!1,this.isSharedProjectionDirty=!1,this.isTransformDirty=!1,this.updateManuallyBlocked=!1,this.updateBlockedByResize=!1,this.isUpdating=!1,this.isSVG=!1,this.needsReset=!1,this.shouldResetTransform=!1,this.hasCheckedOptimisedAppear=!1,this.treeScale={x:1,y:1},this.eventHandlers=new Map,this.hasTreeAnimated=!1,this.layoutVersion=0,this.updateScheduled=!1,this.scheduleUpdate=()=>this.update(),this.projectionUpdateScheduled=!1,this.checkUpdateFailed=()=>{this.isUpdating&&(this.isUpdating=!1,this.clearAllSnapshots())},this.updateProjection=()=>{this.projectionUpdateScheduled=!1,this.nodes.forEach(wS),this.nodes.forEach(zS),this.nodes.forEach(RS),this.nodes.forEach(LS)},this.resolvedRelativeTargetAt=0,this.linkedParentVersion=0,this.hasProjected=!1,this.isVisible=!0,this.animationProgress=0,this.sharedNodes=new Map,this.latestValues=d,this.root=p?p.root||p:this,this.path=p?[...p.path,p]:[],this.parent=p,this.depth=p?p.depth+1:0;for(let y=0;ythis.root.updateBlockedByResize=!1;Dt.read(()=>{b=window.innerWidth}),i(d,()=>{const w=window.innerWidth;w!==b&&(b=w,this.root.updateBlockedByResize=!0,v&&v(),v=TS(T,250),rr.hasAnimatedSinceResize&&(rr.hasAnimatedSinceResize=!1,this.nodes.forEach(Sy)))})}p&&this.root.registerSharedNode(p,this),this.options.animate!==!1&&g&&(p||y)&&this.addEventListener("didUpdate",({delta:v,hasLayoutChanged:b,hasRelativeLayoutChanged:T,layout:w})=>{if(this.isTreeAnimationBlocked()){this.target=void 0,this.relativeTarget=void 0;return}const N=this.options.transition||g.getDefaultTransition()||US,{onLayoutAnimationStart:H,onLayoutAnimationComplete:G}=g.getProps(),U=!this.targetLayout||!pg(this.targetLayout,w),q=!b&&T;if(this.options.layoutRoot||this.resumeFrom||q||b&&(U||!this.currentAnimation)){this.resumeFrom&&(this.resumingFrom=this.resumeFrom,this.resumingFrom.resumingFrom=void 0);const V={...ef(N,"layout"),onPlay:H,onComplete:G};(g.shouldReduceMotion||this.options.layoutRoot)&&(V.delay=0,V.type=!1),this.startAnimation(V),this.setAnimationOrigin(v,q)}else b||Sy(this),this.isLead()&&this.options.onExitComplete&&this.options.onExitComplete();this.targetLayout=w})}unmount(){this.options.layoutId&&this.willUpdate(),this.root.nodes.remove(this);const d=this.getStack();d&&d.remove(this),this.parent&&this.parent.children.delete(this),this.instance=void 0,this.eventHandlers.clear(),Qn(this.updateProjection)}blockUpdate(){this.updateManuallyBlocked=!0}unblockUpdate(){this.updateManuallyBlocked=!1}isUpdateBlocked(){return this.updateManuallyBlocked||this.updateBlockedByResize}isTreeAnimationBlocked(){return this.isAnimationBlocked||this.parent&&this.parent.isTreeAnimationBlocked()||!1}startUpdate(){this.isUpdateBlocked()||(this.isUpdating=!0,this.nodes&&this.nodes.forEach(OS),this.animationId++)}getTransformTemplate(){const{visualElement:d}=this.options;return d&&d.getProps().transformTemplate}willUpdate(d=!0){if(this.root.hasTreeAnimated=!0,this.root.isUpdateBlocked()){this.options.onExitComplete&&this.options.onExitComplete();return}if(window.MotionCancelOptimisedAnimation&&!this.hasCheckedOptimisedAppear&&xg(this),!this.root.isUpdating&&this.root.startUpdate(),this.isLayoutDirty)return;this.isLayoutDirty=!0;for(let v=0;v{this.isLayoutDirty?this.root.didUpdate():this.root.checkUpdateFailed()})}updateSnapshot(){this.snapshot||!this.instance||(this.snapshot=this.measure(),this.snapshot&&!ue(this.snapshot.measuredBox.x)&&!ue(this.snapshot.measuredBox.y)&&(this.snapshot=void 0))}updateLayout(){if(!this.instance||(this.updateScroll(),!(this.options.alwaysMeasureLayout&&this.isLead())&&!this.isLayoutDirty))return;if(this.resumeFrom&&!this.resumeFrom.instance)for(let y=0;y{const Q=Z/1e3;jy(b.x,d.x,Q),jy(b.y,d.y,Q),this.setTargetDelta(b),this.relativeTarget&&this.relativeTargetOrigin&&this.layout&&this.relativeParent&&this.relativeParent.layout&&(hr(T,this.layout.layoutBox,this.relativeParent.layout.layoutBox),VS(this.relativeTarget,this.relativeTargetOrigin,T,Q),V&&mS(this.relativeTarget,V)&&(this.isProjectionDirty=!1),V||(V=Wt()),Ge(V,this.relativeTarget)),H&&(this.animationValues=v,gS(v,g,this.latestValues,Q,q,U)),this.root.scheduleUpdateProjection(),this.scheduleRender(),this.animationProgress=Q},this.mixTargetDelta(this.options.layoutRoot?1e3:0)}startAnimation(d){this.notifyListeners("animationStart"),this.currentAnimation?.stop(),this.resumingFrom?.currentAnimation?.stop(),this.pendingAnimation&&(Qn(this.pendingAnimation),this.pendingAnimation=void 0),this.pendingAnimation=Dt.update(()=>{rr.hasAnimatedSinceResize=!0,this.motionValue||(this.motionValue=ua(0)),this.currentAnimation=bS(this.motionValue,[0,1e3],{...d,velocity:0,isSync:!0,onUpdate:p=>{this.mixTargetDelta(p),d.onUpdate&&d.onUpdate(p)},onStop:()=>{},onComplete:()=>{d.onComplete&&d.onComplete(),this.completeAnimation()}}),this.resumingFrom&&(this.resumingFrom.currentAnimation=this.currentAnimation),this.pendingAnimation=void 0})}completeAnimation(){this.resumingFrom&&(this.resumingFrom.currentAnimation=void 0,this.resumingFrom.preserveOpacity=void 0);const d=this.getStack();d&&d.exitAnimationComplete(),this.resumingFrom=this.currentAnimation=this.animationValues=void 0,this.notifyListeners("animationComplete")}finishAnimation(){this.currentAnimation&&(this.mixTargetDelta&&this.mixTargetDelta(CS),this.currentAnimation.stop()),this.completeAnimation()}applyTransformsToTarget(){const d=this.getLead();let{targetWithTransforms:p,target:y,layout:g,latestValues:v}=d;if(!(!p||!y||!g)){if(this!==d&&this.layout&&g&&bg(this.options.animationType,this.layout.layoutBox,g.layoutBox)){y=this.target||Wt();const b=ue(this.layout.layoutBox.x);y.x.min=d.target.x.min,y.x.max=y.x.min+b;const T=ue(this.layout.layoutBox.y);y.y.min=d.target.y.min,y.y.max=y.y.min+T}Ge(p,y),ra(p,v),yl(this.projectionDeltaWithTransform,this.layoutCorrected,p,v)}}registerSharedNode(d,p){this.sharedNodes.has(d)||this.sharedNodes.set(d,new AS),this.sharedNodes.get(d).add(p);const g=p.options.initialPromotionConfig;p.promote({transition:g?g.transition:void 0,preserveFollowOpacity:g&&g.shouldPreserveFollowOpacity?g.shouldPreserveFollowOpacity(p):void 0})}isLead(){const d=this.getStack();return d?d.lead===this:!0}getLead(){const{layoutId:d}=this.options;return d?this.getStack()?.lead||this:this}getPrevLead(){const{layoutId:d}=this.options;return d?this.getStack()?.prevLead:void 0}getStack(){const{layoutId:d}=this.options;if(d)return this.root.sharedNodes.get(d)}promote({needsReset:d,transition:p,preserveFollowOpacity:y}={}){const g=this.getStack();g&&g.promote(this,y),d&&(this.projectionDelta=void 0,this.needsReset=!0),p&&this.setOptions({transition:p})}relegate(){const d=this.getStack();return d?d.relegate(this):!1}resetSkewAndRotation(){const{visualElement:d}=this.options;if(!d)return;let p=!1;const{latestValues:y}=d;if((y.z||y.rotate||y.rotateX||y.rotateY||y.rotateZ||y.skewX||y.skewY)&&(p=!0),!p)return;const g={};y.z&&tc("z",d,g,this.animationValues);for(let v=0;vd.currentAnimation?.stop()),this.root.nodes.forEach(vy),this.root.sharedNodes.clear()}}}function ES(i){i.updateLayout()}function DS(i){const l=i.resumeFrom?.snapshot||i.snapshot;if(i.isLead()&&i.layout&&l&&i.hasListeners("didUpdate")){const{layoutBox:o,measuredBox:r}=i.layout,{animationType:f}=i.options,m=l.source!==i.layout.source;f==="size"?Ve(v=>{const b=m?l.measuredBox[v]:l.layoutBox[v],T=ue(b);b.min=o[v].min,b.max=b.min+T}):bg(f,l.layoutBox,o)&&Ve(v=>{const b=m?l.measuredBox[v]:l.layoutBox[v],T=ue(o[v]);b.max=b.min+T,i.relativeTarget&&!i.currentAnimation&&(i.isProjectionDirty=!0,i.relativeTarget[v].max=i.relativeTarget[v].min+T)});const d=la();yl(d,o,l.layoutBox);const p=la();m?yl(p,i.applyTransform(r,!0),l.measuredBox):yl(p,o,l.layoutBox);const y=!mg(d);let g=!1;if(!i.resumeFrom){const v=i.getClosestProjectingParent();if(v&&!v.resumeFrom){const{snapshot:b,layout:T}=v;if(b&&T){const w=Wt();hr(w,l.layoutBox,b.layoutBox);const N=Wt();hr(N,o,T.layoutBox),pg(w,N)||(g=!0),v.options.layoutRoot&&(i.relativeTarget=N,i.relativeTargetOrigin=w,i.relativeParent=v)}}}i.notifyListeners("didUpdate",{layout:o,snapshot:l,delta:p,layoutDelta:d,hasLayoutChanged:y,hasRelativeLayoutChanged:g})}else if(i.isLead()){const{onExitComplete:o}=i.options;o&&o()}i.options.transition=void 0}function wS(i){i.parent&&(i.isProjecting()||(i.isProjectionDirty=i.parent.isProjectionDirty),i.isSharedProjectionDirty||(i.isSharedProjectionDirty=!!(i.isProjectionDirty||i.parent.isProjectionDirty||i.parent.isSharedProjectionDirty)),i.isTransformDirty||(i.isTransformDirty=i.parent.isTransformDirty))}function LS(i){i.isProjectionDirty=i.isSharedProjectionDirty=i.isTransformDirty=!1}function NS(i){i.clearSnapshot()}function vy(i){i.clearMeasurements()}function by(i){i.isLayoutDirty=!1}function _S(i){const{visualElement:l}=i.options;l&&l.getProps().onBeforeLayoutMeasure&&l.notify("BeforeLayoutMeasure"),i.resetTransform()}function Sy(i){i.finishAnimation(),i.targetDelta=i.relativeTarget=i.target=void 0,i.isProjectionDirty=!0}function zS(i){i.resolveTargetDelta()}function RS(i){i.calcProjection()}function OS(i){i.resetSkewAndRotation()}function kS(i){i.removeLeadSnapshot()}function jy(i,l,o){i.translate=zt(l.translate,0,o),i.scale=zt(l.scale,1,o),i.origin=l.origin,i.originPoint=l.originPoint}function Ty(i,l,o,r){i.min=zt(l.min,o.min,r),i.max=zt(l.max,o.max,r)}function VS(i,l,o,r){Ty(i.x,l.x,o.x,r),Ty(i.y,l.y,o.y,r)}function BS(i){return i.animationValues&&i.animationValues.opacityExit!==void 0}const US={duration:.45,ease:[.4,0,.1,1]},Ay=i=>typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().includes(i),Cy=Ay("applewebkit/")&&!Ay("chrome/")?Math.round:Ue;function My(i){i.min=Cy(i.min),i.max=Cy(i.max)}function HS(i){My(i.x),My(i.y)}function bg(i,l,o){return i==="position"||i==="preserve-aspect"&&!uS(my(l),my(o),.2)}function YS(i){return i!==i.root&&i.scroll?.wasRoot}const qS=vg({attachResizeListener:(i,l)=>Tl(i,"resize",l),measureScroll:()=>({x:document.documentElement.scrollLeft||document.body?.scrollLeft||0,y:document.documentElement.scrollTop||document.body?.scrollTop||0}),checkIsScrollRoot:()=>!0}),ec={current:void 0},Sg=vg({measureScroll:i=>({x:i.scrollLeft,y:i.scrollTop}),defaultParent:()=>{if(!ec.current){const i=new qS({});i.mount(window),i.setOptions({layoutScroll:!0}),ec.current=i}return ec.current},resetTransform:(i,l)=>{i.style.transform=l!==void 0?l:"none"},checkIsScrollRoot:i=>window.getComputedStyle(i).position==="fixed"}),hf=Y.createContext({transformPagePoint:i=>i,isStatic:!1,reducedMotion:"never"});function Ey(i,l){if(typeof i=="function")return i(l);i!=null&&(i.current=l)}function GS(...i){return l=>{let o=!1;const r=i.map(f=>{const m=Ey(f,l);return!o&&typeof m=="function"&&(o=!0),m});if(o)return()=>{for(let f=0;f{const{width:b,height:T,top:w,left:N,right:H,bottom:G}=p.current;if(l||!d.current||!b||!T)return;const U=o==="left"?`left: ${N}`:`right: ${H}`,q=r==="bottom"?`bottom: ${G}`:`top: ${w}`;d.current.dataset.motionPopId=m;const V=document.createElement("style");y&&(V.nonce=y);const Z=f??document.head;return Z.appendChild(V),V.sheet&&V.sheet.insertRule(` +(function () { + const l = document.createElement('link').relList; + if (l && l.supports && l.supports('modulepreload')) return; + for (const f of document.querySelectorAll('link[rel="modulepreload"]')) r(f); + new MutationObserver((f) => { + for (const m of f) + if (m.type === 'childList') + for (const d of m.addedNodes) d.tagName === 'LINK' && d.rel === 'modulepreload' && r(d); + }).observe(document, { childList: !0, subtree: !0 }); + function o(f) { + const m = {}; + return ( + f.integrity && (m.integrity = f.integrity), + f.referrerPolicy && (m.referrerPolicy = f.referrerPolicy), + f.crossOrigin === 'use-credentials' + ? (m.credentials = 'include') + : f.crossOrigin === 'anonymous' + ? (m.credentials = 'omit') + : (m.credentials = 'same-origin'), + m + ); + } + function r(f) { + if (f.ep) return; + f.ep = !0; + const m = o(f); + fetch(f.href, m); + } +})(); +function lv(i) { + return i && i.__esModule && Object.prototype.hasOwnProperty.call(i, 'default') ? i.default : i; +} +var Uu = { exports: {} }, + sl = {}; +var gp; +function sv() { + if (gp) return sl; + gp = 1; + var i = Symbol.for('react.transitional.element'), + l = Symbol.for('react.fragment'); + function o(r, f, m) { + var d = null; + if ((m !== void 0 && (d = '' + m), f.key !== void 0 && (d = '' + f.key), 'key' in f)) { + m = {}; + for (var p in f) p !== 'key' && (m[p] = f[p]); + } else m = f; + return ((f = m.ref), { $$typeof: i, type: r, key: d, ref: f !== void 0 ? f : null, props: m }); + } + return ((sl.Fragment = l), (sl.jsx = o), (sl.jsxs = o), sl); +} +var xp; +function rv() { + return (xp || ((xp = 1), (Uu.exports = sv())), Uu.exports); +} +var c = rv(), + Hu = { exports: {} }, + at = {}; +var vp; +function ov() { + if (vp) return at; + vp = 1; + var i = Symbol.for('react.transitional.element'), + l = Symbol.for('react.portal'), + o = Symbol.for('react.fragment'), + r = Symbol.for('react.strict_mode'), + f = Symbol.for('react.profiler'), + m = Symbol.for('react.consumer'), + d = Symbol.for('react.context'), + p = Symbol.for('react.forward_ref'), + y = Symbol.for('react.suspense'), + g = Symbol.for('react.memo'), + v = Symbol.for('react.lazy'), + b = Symbol.for('react.activity'), + T = Symbol.iterator; + function w(A) { + return A === null || typeof A != 'object' + ? null + : ((A = (T && A[T]) || A['@@iterator']), typeof A == 'function' ? A : null); + } + var N = { + isMounted: function () { + return !1; + }, + enqueueForceUpdate: function () {}, + enqueueReplaceState: function () {}, + enqueueSetState: function () {}, + }, + H = Object.assign, + G = {}; + function U(A, k, X) { + ((this.props = A), (this.context = k), (this.refs = G), (this.updater = X || N)); + } + ((U.prototype.isReactComponent = {}), + (U.prototype.setState = function (A, k) { + if (typeof A != 'object' && typeof A != 'function' && A != null) + throw Error( + 'takes an object of state variables to update or a function which returns an object of state variables.' + ); + this.updater.enqueueSetState(this, A, k, 'setState'); + }), + (U.prototype.forceUpdate = function (A) { + this.updater.enqueueForceUpdate(this, A, 'forceUpdate'); + })); + function q() {} + q.prototype = U.prototype; + function V(A, k, X) { + ((this.props = A), (this.context = k), (this.refs = G), (this.updater = X || N)); + } + var Z = (V.prototype = new q()); + ((Z.constructor = V), H(Z, U.prototype), (Z.isPureReactComponent = !0)); + var Q = Array.isArray; + function nt() {} + var F = { H: null, A: null, T: null, S: null }, + K = Object.prototype.hasOwnProperty; + function it(A, k, X) { + var $ = X.ref; + return { $$typeof: i, type: A, key: k, ref: $ !== void 0 ? $ : null, props: X }; + } + function yt(A, k) { + return it(A.type, k, A.props); + } + function gt(A) { + return typeof A == 'object' && A !== null && A.$$typeof === i; + } + function Nt(A) { + var k = { '=': '=0', ':': '=2' }; + return ( + '$' + + A.replace(/[=:]/g, function (X) { + return k[X]; + }) + ); + } + var Jt = /\/+/g; + function Ht(A, k) { + return typeof A == 'object' && A !== null && A.key != null ? Nt('' + A.key) : k.toString(36); + } + function Qt(A) { + switch (A.status) { + case 'fulfilled': + return A.value; + case 'rejected': + throw A.reason; + default: + switch ( + (typeof A.status == 'string' + ? A.then(nt, nt) + : ((A.status = 'pending'), + A.then( + function (k) { + A.status === 'pending' && ((A.status = 'fulfilled'), (A.value = k)); + }, + function (k) { + A.status === 'pending' && ((A.status = 'rejected'), (A.reason = k)); + } + )), + A.status) + ) { + case 'fulfilled': + return A.value; + case 'rejected': + throw A.reason; + } + } + throw A; + } + function z(A, k, X, $, lt) { + var ut = typeof A; + (ut === 'undefined' || ut === 'boolean') && (A = null); + var St = !1; + if (A === null) St = !0; + else + switch (ut) { + case 'bigint': + case 'string': + case 'number': + St = !0; + break; + case 'object': + switch (A.$$typeof) { + case i: + case l: + St = !0; + break; + case v: + return ((St = A._init), z(St(A._payload), k, X, $, lt)); + } + } + if (St) + return ( + (lt = lt(A)), + (St = $ === '' ? '.' + Ht(A, 0) : $), + Q(lt) + ? ((X = ''), + St != null && (X = St.replace(Jt, '$&/') + '/'), + z(lt, k, X, '', function (ha) { + return ha; + })) + : lt != null && + (gt(lt) && + (lt = yt( + lt, + X + + (lt.key == null || (A && A.key === lt.key) + ? '' + : ('' + lt.key).replace(Jt, '$&/') + '/') + + St + )), + k.push(lt)), + 1 + ); + St = 0; + var se = $ === '' ? '.' : $ + ':'; + if (Q(A)) + for (var Vt = 0; Vt < A.length; Vt++) + (($ = A[Vt]), (ut = se + Ht($, Vt)), (St += z($, k, X, ut, lt))); + else if (((Vt = w(A)), typeof Vt == 'function')) + for (A = Vt.call(A), Vt = 0; !($ = A.next()).done; ) + (($ = $.value), (ut = se + Ht($, Vt++)), (St += z($, k, X, ut, lt))); + else if (ut === 'object') { + if (typeof A.then == 'function') return z(Qt(A), k, X, $, lt); + throw ( + (k = String(A)), + Error( + 'Objects are not valid as a React child (found: ' + + (k === '[object Object]' ? 'object with keys {' + Object.keys(A).join(', ') + '}' : k) + + '). If you meant to render a collection of children, use an array instead.' + ) + ); + } + return St; + } + function B(A, k, X) { + if (A == null) return A; + var $ = [], + lt = 0; + return ( + z(A, $, '', '', function (ut) { + return k.call(X, ut, lt++); + }), + $ + ); + } + function P(A) { + if (A._status === -1) { + var k = A._result; + ((k = k()), + k.then( + function (X) { + (A._status === 0 || A._status === -1) && ((A._status = 1), (A._result = X)); + }, + function (X) { + (A._status === 0 || A._status === -1) && ((A._status = 2), (A._result = X)); + } + ), + A._status === -1 && ((A._status = 0), (A._result = k))); + } + if (A._status === 1) return A._result.default; + throw A._result; + } + var ot = + typeof reportError == 'function' + ? reportError + : function (A) { + if (typeof window == 'object' && typeof window.ErrorEvent == 'function') { + var k = new window.ErrorEvent('error', { + bubbles: !0, + cancelable: !0, + message: + typeof A == 'object' && A !== null && typeof A.message == 'string' + ? String(A.message) + : String(A), + error: A, + }); + if (!window.dispatchEvent(k)) return; + } else if (typeof process == 'object' && typeof process.emit == 'function') { + process.emit('uncaughtException', A); + return; + } + console.error(A); + }, + dt = { + map: B, + forEach: function (A, k, X) { + B( + A, + function () { + k.apply(this, arguments); + }, + X + ); + }, + count: function (A) { + var k = 0; + return ( + B(A, function () { + k++; + }), + k + ); + }, + toArray: function (A) { + return ( + B(A, function (k) { + return k; + }) || [] + ); + }, + only: function (A) { + if (!gt(A)) + throw Error('React.Children.only expected to receive a single React element child.'); + return A; + }, + }; + return ( + (at.Activity = b), + (at.Children = dt), + (at.Component = U), + (at.Fragment = o), + (at.Profiler = f), + (at.PureComponent = V), + (at.StrictMode = r), + (at.Suspense = y), + (at.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = F), + (at.__COMPILER_RUNTIME = { + __proto__: null, + c: function (A) { + return F.H.useMemoCache(A); + }, + }), + (at.cache = function (A) { + return function () { + return A.apply(null, arguments); + }; + }), + (at.cacheSignal = function () { + return null; + }), + (at.cloneElement = function (A, k, X) { + if (A == null) throw Error('The argument must be a React element, but you passed ' + A + '.'); + var $ = H({}, A.props), + lt = A.key; + if (k != null) + for (ut in (k.key !== void 0 && (lt = '' + k.key), k)) + !K.call(k, ut) || + ut === 'key' || + ut === '__self' || + ut === '__source' || + (ut === 'ref' && k.ref === void 0) || + ($[ut] = k[ut]); + var ut = arguments.length - 2; + if (ut === 1) $.children = X; + else if (1 < ut) { + for (var St = Array(ut), se = 0; se < ut; se++) St[se] = arguments[se + 2]; + $.children = St; + } + return it(A.type, lt, $); + }), + (at.createContext = function (A) { + return ( + (A = { + $$typeof: d, + _currentValue: A, + _currentValue2: A, + _threadCount: 0, + Provider: null, + Consumer: null, + }), + (A.Provider = A), + (A.Consumer = { $$typeof: m, _context: A }), + A + ); + }), + (at.createElement = function (A, k, X) { + var $, + lt = {}, + ut = null; + if (k != null) + for ($ in (k.key !== void 0 && (ut = '' + k.key), k)) + K.call(k, $) && $ !== 'key' && $ !== '__self' && $ !== '__source' && (lt[$] = k[$]); + var St = arguments.length - 2; + if (St === 1) lt.children = X; + else if (1 < St) { + for (var se = Array(St), Vt = 0; Vt < St; Vt++) se[Vt] = arguments[Vt + 2]; + lt.children = se; + } + if (A && A.defaultProps) + for ($ in ((St = A.defaultProps), St)) lt[$] === void 0 && (lt[$] = St[$]); + return it(A, ut, lt); + }), + (at.createRef = function () { + return { current: null }; + }), + (at.forwardRef = function (A) { + return { $$typeof: p, render: A }; + }), + (at.isValidElement = gt), + (at.lazy = function (A) { + return { $$typeof: v, _payload: { _status: -1, _result: A }, _init: P }; + }), + (at.memo = function (A, k) { + return { $$typeof: g, type: A, compare: k === void 0 ? null : k }; + }), + (at.startTransition = function (A) { + var k = F.T, + X = {}; + F.T = X; + try { + var $ = A(), + lt = F.S; + (lt !== null && lt(X, $), + typeof $ == 'object' && $ !== null && typeof $.then == 'function' && $.then(nt, ot)); + } catch (ut) { + ot(ut); + } finally { + (k !== null && X.types !== null && (k.types = X.types), (F.T = k)); + } + }), + (at.unstable_useCacheRefresh = function () { + return F.H.useCacheRefresh(); + }), + (at.use = function (A) { + return F.H.use(A); + }), + (at.useActionState = function (A, k, X) { + return F.H.useActionState(A, k, X); + }), + (at.useCallback = function (A, k) { + return F.H.useCallback(A, k); + }), + (at.useContext = function (A) { + return F.H.useContext(A); + }), + (at.useDebugValue = function () {}), + (at.useDeferredValue = function (A, k) { + return F.H.useDeferredValue(A, k); + }), + (at.useEffect = function (A, k) { + return F.H.useEffect(A, k); + }), + (at.useEffectEvent = function (A) { + return F.H.useEffectEvent(A); + }), + (at.useId = function () { + return F.H.useId(); + }), + (at.useImperativeHandle = function (A, k, X) { + return F.H.useImperativeHandle(A, k, X); + }), + (at.useInsertionEffect = function (A, k) { + return F.H.useInsertionEffect(A, k); + }), + (at.useLayoutEffect = function (A, k) { + return F.H.useLayoutEffect(A, k); + }), + (at.useMemo = function (A, k) { + return F.H.useMemo(A, k); + }), + (at.useOptimistic = function (A, k) { + return F.H.useOptimistic(A, k); + }), + (at.useReducer = function (A, k, X) { + return F.H.useReducer(A, k, X); + }), + (at.useRef = function (A) { + return F.H.useRef(A); + }), + (at.useState = function (A) { + return F.H.useState(A); + }), + (at.useSyncExternalStore = function (A, k, X) { + return F.H.useSyncExternalStore(A, k, X); + }), + (at.useTransition = function () { + return F.H.useTransition(); + }), + (at.version = '19.2.4'), + at + ); +} +var bp; +function kc() { + return (bp || ((bp = 1), (Hu.exports = ov())), Hu.exports); +} +var Y = kc(); +const Xn = lv(Y); +var Yu = { exports: {} }, + rl = {}, + qu = { exports: {} }, + Gu = {}; +var Sp; +function uv() { + return ( + Sp || + ((Sp = 1), + (function (i) { + function l(z, B) { + var P = z.length; + z.push(B); + t: for (; 0 < P; ) { + var ot = (P - 1) >>> 1, + dt = z[ot]; + if (0 < f(dt, B)) ((z[ot] = B), (z[P] = dt), (P = ot)); + else break t; + } + } + function o(z) { + return z.length === 0 ? null : z[0]; + } + function r(z) { + if (z.length === 0) return null; + var B = z[0], + P = z.pop(); + if (P !== B) { + z[0] = P; + t: for (var ot = 0, dt = z.length, A = dt >>> 1; ot < A; ) { + var k = 2 * (ot + 1) - 1, + X = z[k], + $ = k + 1, + lt = z[$]; + if (0 > f(X, P)) + $ < dt && 0 > f(lt, X) + ? ((z[ot] = lt), (z[$] = P), (ot = $)) + : ((z[ot] = X), (z[k] = P), (ot = k)); + else if ($ < dt && 0 > f(lt, P)) ((z[ot] = lt), (z[$] = P), (ot = $)); + else break t; + } + } + return B; + } + function f(z, B) { + var P = z.sortIndex - B.sortIndex; + return P !== 0 ? P : z.id - B.id; + } + if ( + ((i.unstable_now = void 0), + typeof performance == 'object' && typeof performance.now == 'function') + ) { + var m = performance; + i.unstable_now = function () { + return m.now(); + }; + } else { + var d = Date, + p = d.now(); + i.unstable_now = function () { + return d.now() - p; + }; + } + var y = [], + g = [], + v = 1, + b = null, + T = 3, + w = !1, + N = !1, + H = !1, + G = !1, + U = typeof setTimeout == 'function' ? setTimeout : null, + q = typeof clearTimeout == 'function' ? clearTimeout : null, + V = typeof setImmediate < 'u' ? setImmediate : null; + function Z(z) { + for (var B = o(g); B !== null; ) { + if (B.callback === null) r(g); + else if (B.startTime <= z) (r(g), (B.sortIndex = B.expirationTime), l(y, B)); + else break; + B = o(g); + } + } + function Q(z) { + if (((H = !1), Z(z), !N)) + if (o(y) !== null) ((N = !0), nt || ((nt = !0), Nt())); + else { + var B = o(g); + B !== null && Qt(Q, B.startTime - z); + } + } + var nt = !1, + F = -1, + K = 5, + it = -1; + function yt() { + return G ? !0 : !(i.unstable_now() - it < K); + } + function gt() { + if (((G = !1), nt)) { + var z = i.unstable_now(); + it = z; + var B = !0; + try { + t: { + ((N = !1), H && ((H = !1), q(F), (F = -1)), (w = !0)); + var P = T; + try { + e: { + for (Z(z), b = o(y); b !== null && !(b.expirationTime > z && yt()); ) { + var ot = b.callback; + if (typeof ot == 'function') { + ((b.callback = null), (T = b.priorityLevel)); + var dt = ot(b.expirationTime <= z); + if (((z = i.unstable_now()), typeof dt == 'function')) { + ((b.callback = dt), Z(z), (B = !0)); + break e; + } + (b === o(y) && r(y), Z(z)); + } else r(y); + b = o(y); + } + if (b !== null) B = !0; + else { + var A = o(g); + (A !== null && Qt(Q, A.startTime - z), (B = !1)); + } + } + break t; + } finally { + ((b = null), (T = P), (w = !1)); + } + B = void 0; + } + } finally { + B ? Nt() : (nt = !1); + } + } + } + var Nt; + if (typeof V == 'function') + Nt = function () { + V(gt); + }; + else if (typeof MessageChannel < 'u') { + var Jt = new MessageChannel(), + Ht = Jt.port2; + ((Jt.port1.onmessage = gt), + (Nt = function () { + Ht.postMessage(null); + })); + } else + Nt = function () { + U(gt, 0); + }; + function Qt(z, B) { + F = U(function () { + z(i.unstable_now()); + }, B); + } + ((i.unstable_IdlePriority = 5), + (i.unstable_ImmediatePriority = 1), + (i.unstable_LowPriority = 4), + (i.unstable_NormalPriority = 3), + (i.unstable_Profiling = null), + (i.unstable_UserBlockingPriority = 2), + (i.unstable_cancelCallback = function (z) { + z.callback = null; + }), + (i.unstable_forceFrameRate = function (z) { + 0 > z || 125 < z + ? console.error( + 'forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported' + ) + : (K = 0 < z ? Math.floor(1e3 / z) : 5); + }), + (i.unstable_getCurrentPriorityLevel = function () { + return T; + }), + (i.unstable_next = function (z) { + switch (T) { + case 1: + case 2: + case 3: + var B = 3; + break; + default: + B = T; + } + var P = T; + T = B; + try { + return z(); + } finally { + T = P; + } + }), + (i.unstable_requestPaint = function () { + G = !0; + }), + (i.unstable_runWithPriority = function (z, B) { + switch (z) { + case 1: + case 2: + case 3: + case 4: + case 5: + break; + default: + z = 3; + } + var P = T; + T = z; + try { + return B(); + } finally { + T = P; + } + }), + (i.unstable_scheduleCallback = function (z, B, P) { + var ot = i.unstable_now(); + switch ( + (typeof P == 'object' && P !== null + ? ((P = P.delay), (P = typeof P == 'number' && 0 < P ? ot + P : ot)) + : (P = ot), + z) + ) { + case 1: + var dt = -1; + break; + case 2: + dt = 250; + break; + case 5: + dt = 1073741823; + break; + case 4: + dt = 1e4; + break; + default: + dt = 5e3; + } + return ( + (dt = P + dt), + (z = { + id: v++, + callback: B, + priorityLevel: z, + startTime: P, + expirationTime: dt, + sortIndex: -1, + }), + P > ot + ? ((z.sortIndex = P), + l(g, z), + o(y) === null && z === o(g) && (H ? (q(F), (F = -1)) : (H = !0), Qt(Q, P - ot))) + : ((z.sortIndex = dt), l(y, z), N || w || ((N = !0), nt || ((nt = !0), Nt()))), + z + ); + }), + (i.unstable_shouldYield = yt), + (i.unstable_wrapCallback = function (z) { + var B = T; + return function () { + var P = T; + T = B; + try { + return z.apply(this, arguments); + } finally { + T = P; + } + }; + })); + })(Gu)), + Gu + ); +} +var jp; +function cv() { + return (jp || ((jp = 1), (qu.exports = uv())), qu.exports); +} +var Xu = { exports: {} }, + ae = {}; +var Tp; +function fv() { + if (Tp) return ae; + Tp = 1; + var i = kc(); + function l(y) { + var g = 'https://react.dev/errors/' + y; + if (1 < arguments.length) { + g += '?args[]=' + encodeURIComponent(arguments[1]); + for (var v = 2; v < arguments.length; v++) g += '&args[]=' + encodeURIComponent(arguments[v]); + } + return ( + 'Minified React error #' + + y + + '; visit ' + + g + + ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' + ); + } + function o() {} + var r = { + d: { + f: o, + r: function () { + throw Error(l(522)); + }, + D: o, + C: o, + L: o, + m: o, + X: o, + S: o, + M: o, + }, + p: 0, + findDOMNode: null, + }, + f = Symbol.for('react.portal'); + function m(y, g, v) { + var b = 3 < arguments.length && arguments[3] !== void 0 ? arguments[3] : null; + return { + $$typeof: f, + key: b == null ? null : '' + b, + children: y, + containerInfo: g, + implementation: v, + }; + } + var d = i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; + function p(y, g) { + if (y === 'font') return ''; + if (typeof g == 'string') return g === 'use-credentials' ? g : ''; + } + return ( + (ae.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = r), + (ae.createPortal = function (y, g) { + var v = 2 < arguments.length && arguments[2] !== void 0 ? arguments[2] : null; + if (!g || (g.nodeType !== 1 && g.nodeType !== 9 && g.nodeType !== 11)) throw Error(l(299)); + return m(y, g, null, v); + }), + (ae.flushSync = function (y) { + var g = d.T, + v = r.p; + try { + if (((d.T = null), (r.p = 2), y)) return y(); + } finally { + ((d.T = g), (r.p = v), r.d.f()); + } + }), + (ae.preconnect = function (y, g) { + typeof y == 'string' && + (g + ? ((g = g.crossOrigin), + (g = typeof g == 'string' ? (g === 'use-credentials' ? g : '') : void 0)) + : (g = null), + r.d.C(y, g)); + }), + (ae.prefetchDNS = function (y) { + typeof y == 'string' && r.d.D(y); + }), + (ae.preinit = function (y, g) { + if (typeof y == 'string' && g && typeof g.as == 'string') { + var v = g.as, + b = p(v, g.crossOrigin), + T = typeof g.integrity == 'string' ? g.integrity : void 0, + w = typeof g.fetchPriority == 'string' ? g.fetchPriority : void 0; + v === 'style' + ? r.d.S(y, typeof g.precedence == 'string' ? g.precedence : void 0, { + crossOrigin: b, + integrity: T, + fetchPriority: w, + }) + : v === 'script' && + r.d.X(y, { + crossOrigin: b, + integrity: T, + fetchPriority: w, + nonce: typeof g.nonce == 'string' ? g.nonce : void 0, + }); + } + }), + (ae.preinitModule = function (y, g) { + if (typeof y == 'string') + if (typeof g == 'object' && g !== null) { + if (g.as == null || g.as === 'script') { + var v = p(g.as, g.crossOrigin); + r.d.M(y, { + crossOrigin: v, + integrity: typeof g.integrity == 'string' ? g.integrity : void 0, + nonce: typeof g.nonce == 'string' ? g.nonce : void 0, + }); + } + } else g == null && r.d.M(y); + }), + (ae.preload = function (y, g) { + if (typeof y == 'string' && typeof g == 'object' && g !== null && typeof g.as == 'string') { + var v = g.as, + b = p(v, g.crossOrigin); + r.d.L(y, v, { + crossOrigin: b, + integrity: typeof g.integrity == 'string' ? g.integrity : void 0, + nonce: typeof g.nonce == 'string' ? g.nonce : void 0, + type: typeof g.type == 'string' ? g.type : void 0, + fetchPriority: typeof g.fetchPriority == 'string' ? g.fetchPriority : void 0, + referrerPolicy: typeof g.referrerPolicy == 'string' ? g.referrerPolicy : void 0, + imageSrcSet: typeof g.imageSrcSet == 'string' ? g.imageSrcSet : void 0, + imageSizes: typeof g.imageSizes == 'string' ? g.imageSizes : void 0, + media: typeof g.media == 'string' ? g.media : void 0, + }); + } + }), + (ae.preloadModule = function (y, g) { + if (typeof y == 'string') + if (g) { + var v = p(g.as, g.crossOrigin); + r.d.m(y, { + as: typeof g.as == 'string' && g.as !== 'script' ? g.as : void 0, + crossOrigin: v, + integrity: typeof g.integrity == 'string' ? g.integrity : void 0, + }); + } else r.d.m(y); + }), + (ae.requestFormReset = function (y) { + r.d.r(y); + }), + (ae.unstable_batchedUpdates = function (y, g) { + return y(g); + }), + (ae.useFormState = function (y, g, v) { + return d.H.useFormState(y, g, v); + }), + (ae.useFormStatus = function () { + return d.H.useHostTransitionStatus(); + }), + (ae.version = '19.2.4'), + ae + ); +} +var Ap; +function dv() { + if (Ap) return Xu.exports; + Ap = 1; + function i() { + if ( + !( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ > 'u' || + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE != 'function' + ) + ) + try { + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i); + } catch (l) { + console.error(l); + } + } + return (i(), (Xu.exports = fv()), Xu.exports); +} +var Cp; +function hv() { + if (Cp) return rl; + Cp = 1; + var i = cv(), + l = kc(), + o = dv(); + function r(t) { + var e = 'https://react.dev/errors/' + t; + if (1 < arguments.length) { + e += '?args[]=' + encodeURIComponent(arguments[1]); + for (var n = 2; n < arguments.length; n++) e += '&args[]=' + encodeURIComponent(arguments[n]); + } + return ( + 'Minified React error #' + + t + + '; visit ' + + e + + ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' + ); + } + function f(t) { + return !(!t || (t.nodeType !== 1 && t.nodeType !== 9 && t.nodeType !== 11)); + } + function m(t) { + var e = t, + n = t; + if (t.alternate) for (; e.return; ) e = e.return; + else { + t = e; + do ((e = t), (e.flags & 4098) !== 0 && (n = e.return), (t = e.return)); + while (t); + } + return e.tag === 3 ? n : null; + } + function d(t) { + if (t.tag === 13) { + var e = t.memoizedState; + if ((e === null && ((t = t.alternate), t !== null && (e = t.memoizedState)), e !== null)) + return e.dehydrated; + } + return null; + } + function p(t) { + if (t.tag === 31) { + var e = t.memoizedState; + if ((e === null && ((t = t.alternate), t !== null && (e = t.memoizedState)), e !== null)) + return e.dehydrated; + } + return null; + } + function y(t) { + if (m(t) !== t) throw Error(r(188)); + } + function g(t) { + var e = t.alternate; + if (!e) { + if (((e = m(t)), e === null)) throw Error(r(188)); + return e !== t ? null : t; + } + for (var n = t, a = e; ; ) { + var s = n.return; + if (s === null) break; + var u = s.alternate; + if (u === null) { + if (((a = s.return), a !== null)) { + n = a; + continue; + } + break; + } + if (s.child === u.child) { + for (u = s.child; u; ) { + if (u === n) return (y(s), t); + if (u === a) return (y(s), e); + u = u.sibling; + } + throw Error(r(188)); + } + if (n.return !== a.return) ((n = s), (a = u)); + else { + for (var h = !1, x = s.child; x; ) { + if (x === n) { + ((h = !0), (n = s), (a = u)); + break; + } + if (x === a) { + ((h = !0), (a = s), (n = u)); + break; + } + x = x.sibling; + } + if (!h) { + for (x = u.child; x; ) { + if (x === n) { + ((h = !0), (n = u), (a = s)); + break; + } + if (x === a) { + ((h = !0), (a = u), (n = s)); + break; + } + x = x.sibling; + } + if (!h) throw Error(r(189)); + } + } + if (n.alternate !== a) throw Error(r(190)); + } + if (n.tag !== 3) throw Error(r(188)); + return n.stateNode.current === n ? t : e; + } + function v(t) { + var e = t.tag; + if (e === 5 || e === 26 || e === 27 || e === 6) return t; + for (t = t.child; t !== null; ) { + if (((e = v(t)), e !== null)) return e; + t = t.sibling; + } + return null; + } + var b = Object.assign, + T = Symbol.for('react.element'), + w = Symbol.for('react.transitional.element'), + N = Symbol.for('react.portal'), + H = Symbol.for('react.fragment'), + G = Symbol.for('react.strict_mode'), + U = Symbol.for('react.profiler'), + q = Symbol.for('react.consumer'), + V = Symbol.for('react.context'), + Z = Symbol.for('react.forward_ref'), + Q = Symbol.for('react.suspense'), + nt = Symbol.for('react.suspense_list'), + F = Symbol.for('react.memo'), + K = Symbol.for('react.lazy'), + it = Symbol.for('react.activity'), + yt = Symbol.for('react.memo_cache_sentinel'), + gt = Symbol.iterator; + function Nt(t) { + return t === null || typeof t != 'object' + ? null + : ((t = (gt && t[gt]) || t['@@iterator']), typeof t == 'function' ? t : null); + } + var Jt = Symbol.for('react.client.reference'); + function Ht(t) { + if (t == null) return null; + if (typeof t == 'function') return t.$$typeof === Jt ? null : t.displayName || t.name || null; + if (typeof t == 'string') return t; + switch (t) { + case H: + return 'Fragment'; + case U: + return 'Profiler'; + case G: + return 'StrictMode'; + case Q: + return 'Suspense'; + case nt: + return 'SuspenseList'; + case it: + return 'Activity'; + } + if (typeof t == 'object') + switch (t.$$typeof) { + case N: + return 'Portal'; + case V: + return t.displayName || 'Context'; + case q: + return (t._context.displayName || 'Context') + '.Consumer'; + case Z: + var e = t.render; + return ( + (t = t.displayName), + t || + ((t = e.displayName || e.name || ''), + (t = t !== '' ? 'ForwardRef(' + t + ')' : 'ForwardRef')), + t + ); + case F: + return ((e = t.displayName || null), e !== null ? e : Ht(t.type) || 'Memo'); + case K: + ((e = t._payload), (t = t._init)); + try { + return Ht(t(e)); + } catch {} + } + return null; + } + var Qt = Array.isArray, + z = l.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, + B = o.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, + P = { pending: !1, data: null, method: null, action: null }, + ot = [], + dt = -1; + function A(t) { + return { current: t }; + } + function k(t) { + 0 > dt || ((t.current = ot[dt]), (ot[dt] = null), dt--); + } + function X(t, e) { + (dt++, (ot[dt] = t.current), (t.current = e)); + } + var $ = A(null), + lt = A(null), + ut = A(null), + St = A(null); + function se(t, e) { + switch ((X(ut, e), X(lt, t), X($, null), e.nodeType)) { + case 9: + case 11: + t = (t = e.documentElement) && (t = t.namespaceURI) ? Hm(t) : 0; + break; + default: + if (((t = e.tagName), (e = e.namespaceURI))) ((e = Hm(e)), (t = Ym(e, t))); + else + switch (t) { + case 'svg': + t = 1; + break; + case 'math': + t = 2; + break; + default: + t = 0; + } + } + (k($), X($, t)); + } + function Vt() { + (k($), k(lt), k(ut)); + } + function ha(t) { + t.memoizedState !== null && X(St, t); + var e = $.current, + n = Ym(e, t.type); + e !== n && (X(lt, t), X($, n)); + } + function Dl(t) { + (lt.current === t && (k($), k(lt)), St.current === t && (k(St), (nl._currentValue = P))); + } + var br, yf; + function Fn(t) { + if (br === void 0) + try { + throw Error(); + } catch (n) { + var e = n.stack.trim().match(/\n( *(at )?)/); + ((br = (e && e[1]) || ''), + (yf = + -1 < + n.stack.indexOf(` + at`) + ? ' ()' + : -1 < n.stack.indexOf('@') + ? '@unknown:0:0' + : '')); + } + return ( + ` +` + + br + + t + + yf + ); + } + var Sr = !1; + function jr(t, e) { + if (!t || Sr) return ''; + Sr = !0; + var n = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + try { + var a = { + DetermineComponentFrameRoot: function () { + try { + if (e) { + var O = function () { + throw Error(); + }; + if ( + (Object.defineProperty(O.prototype, 'props', { + set: function () { + throw Error(); + }, + }), + typeof Reflect == 'object' && Reflect.construct) + ) { + try { + Reflect.construct(O, []); + } catch (L) { + var D = L; + } + Reflect.construct(t, [], O); + } else { + try { + O.call(); + } catch (L) { + D = L; + } + t.call(O.prototype); + } + } else { + try { + throw Error(); + } catch (L) { + D = L; + } + (O = t()) && typeof O.catch == 'function' && O.catch(function () {}); + } + } catch (L) { + if (L && D && typeof L.stack == 'string') return [L.stack, D.stack]; + } + return [null, null]; + }, + }; + a.DetermineComponentFrameRoot.displayName = 'DetermineComponentFrameRoot'; + var s = Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot, 'name'); + s && + s.configurable && + Object.defineProperty(a.DetermineComponentFrameRoot, 'name', { + value: 'DetermineComponentFrameRoot', + }); + var u = a.DetermineComponentFrameRoot(), + h = u[0], + x = u[1]; + if (h && x) { + var S = h.split(` +`), + E = x.split(` +`); + for (s = a = 0; a < S.length && !S[a].includes('DetermineComponentFrameRoot'); ) a++; + for (; s < E.length && !E[s].includes('DetermineComponentFrameRoot'); ) s++; + if (a === S.length || s === E.length) + for (a = S.length - 1, s = E.length - 1; 1 <= a && 0 <= s && S[a] !== E[s]; ) s--; + for (; 1 <= a && 0 <= s; a--, s--) + if (S[a] !== E[s]) { + if (a !== 1 || s !== 1) + do + if ((a--, s--, 0 > s || S[a] !== E[s])) { + var _ = + ` +` + S[a].replace(' at new ', ' at '); + return ( + t.displayName && + _.includes('') && + (_ = _.replace('', t.displayName)), + _ + ); + } + while (1 <= a && 0 <= s); + break; + } + } + } finally { + ((Sr = !1), (Error.prepareStackTrace = n)); + } + return (n = t ? t.displayName || t.name : '') ? Fn(n) : ''; + } + function kg(t, e) { + switch (t.tag) { + case 26: + case 27: + case 5: + return Fn(t.type); + case 16: + return Fn('Lazy'); + case 13: + return t.child !== e && e !== null ? Fn('Suspense Fallback') : Fn('Suspense'); + case 19: + return Fn('SuspenseList'); + case 0: + case 15: + return jr(t.type, !1); + case 11: + return jr(t.type.render, !1); + case 1: + return jr(t.type, !0); + case 31: + return Fn('Activity'); + default: + return ''; + } + } + function gf(t) { + try { + var e = '', + n = null; + do ((e += kg(t, n)), (n = t), (t = t.return)); + while (t); + return e; + } catch (a) { + return ( + ` +Error generating stack: ` + + a.message + + ` +` + + a.stack + ); + } + } + var Tr = Object.prototype.hasOwnProperty, + Ar = i.unstable_scheduleCallback, + Cr = i.unstable_cancelCallback, + Vg = i.unstable_shouldYield, + Bg = i.unstable_requestPaint, + ge = i.unstable_now, + Ug = i.unstable_getCurrentPriorityLevel, + xf = i.unstable_ImmediatePriority, + vf = i.unstable_UserBlockingPriority, + wl = i.unstable_NormalPriority, + Hg = i.unstable_LowPriority, + bf = i.unstable_IdlePriority, + Yg = i.log, + qg = i.unstable_setDisableYieldValue, + ma = null, + xe = null; + function xn(t) { + if ((typeof Yg == 'function' && qg(t), xe && typeof xe.setStrictMode == 'function')) + try { + xe.setStrictMode(ma, t); + } catch {} + } + var ve = Math.clz32 ? Math.clz32 : Zg, + Gg = Math.log, + Xg = Math.LN2; + function Zg(t) { + return ((t >>>= 0), t === 0 ? 32 : (31 - ((Gg(t) / Xg) | 0)) | 0); + } + var Ll = 256, + Nl = 262144, + _l = 4194304; + function Pn(t) { + var e = t & 42; + if (e !== 0) return e; + switch (t & -t) { + case 1: + return 1; + case 2: + return 2; + case 4: + return 4; + case 8: + return 8; + case 16: + return 16; + case 32: + return 32; + case 64: + return 64; + case 128: + return 128; + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + return t & 261888; + case 262144: + case 524288: + case 1048576: + case 2097152: + return t & 3932160; + case 4194304: + case 8388608: + case 16777216: + case 33554432: + return t & 62914560; + case 67108864: + return 67108864; + case 134217728: + return 134217728; + case 268435456: + return 268435456; + case 536870912: + return 536870912; + case 1073741824: + return 0; + default: + return t; + } + } + function zl(t, e, n) { + var a = t.pendingLanes; + if (a === 0) return 0; + var s = 0, + u = t.suspendedLanes, + h = t.pingedLanes; + t = t.warmLanes; + var x = a & 134217727; + return ( + x !== 0 + ? ((a = x & ~u), + a !== 0 + ? (s = Pn(a)) + : ((h &= x), h !== 0 ? (s = Pn(h)) : n || ((n = x & ~t), n !== 0 && (s = Pn(n))))) + : ((x = a & ~u), + x !== 0 + ? (s = Pn(x)) + : h !== 0 + ? (s = Pn(h)) + : n || ((n = a & ~t), n !== 0 && (s = Pn(n)))), + s === 0 + ? 0 + : e !== 0 && + e !== s && + (e & u) === 0 && + ((u = s & -s), (n = e & -e), u >= n || (u === 32 && (n & 4194048) !== 0)) + ? e + : s + ); + } + function pa(t, e) { + return (t.pendingLanes & ~(t.suspendedLanes & ~t.pingedLanes) & e) === 0; + } + function Qg(t, e) { + switch (t) { + case 1: + case 2: + case 4: + case 8: + case 64: + return e + 250; + case 16: + case 32: + case 128: + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + return e + 5e3; + case 4194304: + case 8388608: + case 16777216: + case 33554432: + return -1; + case 67108864: + case 134217728: + case 268435456: + case 536870912: + case 1073741824: + return -1; + default: + return -1; + } + } + function Sf() { + var t = _l; + return ((_l <<= 1), (_l & 62914560) === 0 && (_l = 4194304), t); + } + function Mr(t) { + for (var e = [], n = 0; 31 > n; n++) e.push(t); + return e; + } + function ya(t, e) { + ((t.pendingLanes |= e), + e !== 268435456 && ((t.suspendedLanes = 0), (t.pingedLanes = 0), (t.warmLanes = 0))); + } + function Kg(t, e, n, a, s, u) { + var h = t.pendingLanes; + ((t.pendingLanes = n), + (t.suspendedLanes = 0), + (t.pingedLanes = 0), + (t.warmLanes = 0), + (t.expiredLanes &= n), + (t.entangledLanes &= n), + (t.errorRecoveryDisabledLanes &= n), + (t.shellSuspendCounter = 0)); + var x = t.entanglements, + S = t.expirationTimes, + E = t.hiddenUpdates; + for (n = h & ~n; 0 < n; ) { + var _ = 31 - ve(n), + O = 1 << _; + ((x[_] = 0), (S[_] = -1)); + var D = E[_]; + if (D !== null) + for (E[_] = null, _ = 0; _ < D.length; _++) { + var L = D[_]; + L !== null && (L.lane &= -536870913); + } + n &= ~O; + } + (a !== 0 && jf(t, a, 0), + u !== 0 && s === 0 && t.tag !== 0 && (t.suspendedLanes |= u & ~(h & ~e))); + } + function jf(t, e, n) { + ((t.pendingLanes |= e), (t.suspendedLanes &= ~e)); + var a = 31 - ve(e); + ((t.entangledLanes |= e), + (t.entanglements[a] = t.entanglements[a] | 1073741824 | (n & 261930))); + } + function Tf(t, e) { + var n = (t.entangledLanes |= e); + for (t = t.entanglements; n; ) { + var a = 31 - ve(n), + s = 1 << a; + ((s & e) | (t[a] & e) && (t[a] |= e), (n &= ~s)); + } + } + function Af(t, e) { + var n = e & -e; + return ((n = (n & 42) !== 0 ? 1 : Er(n)), (n & (t.suspendedLanes | e)) !== 0 ? 0 : n); + } + function Er(t) { + switch (t) { + case 2: + t = 1; + break; + case 8: + t = 4; + break; + case 32: + t = 16; + break; + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + case 4194304: + case 8388608: + case 16777216: + case 33554432: + t = 128; + break; + case 268435456: + t = 134217728; + break; + default: + t = 0; + } + return t; + } + function Dr(t) { + return ((t &= -t), 2 < t ? (8 < t ? ((t & 134217727) !== 0 ? 32 : 268435456) : 8) : 2); + } + function Cf() { + var t = B.p; + return t !== 0 ? t : ((t = window.event), t === void 0 ? 32 : cp(t.type)); + } + function Mf(t, e) { + var n = B.p; + try { + return ((B.p = t), e()); + } finally { + B.p = n; + } + } + var vn = Math.random().toString(36).slice(2), + $t = '__reactFiber$' + vn, + ce = '__reactProps$' + vn, + bi = '__reactContainer$' + vn, + wr = '__reactEvents$' + vn, + Wg = '__reactListeners$' + vn, + Jg = '__reactHandles$' + vn, + Ef = '__reactResources$' + vn, + ga = '__reactMarker$' + vn; + function Lr(t) { + (delete t[$t], delete t[ce], delete t[wr], delete t[Wg], delete t[Jg]); + } + function Si(t) { + var e = t[$t]; + if (e) return e; + for (var n = t.parentNode; n; ) { + if ((e = n[bi] || n[$t])) { + if (((n = e.alternate), e.child !== null || (n !== null && n.child !== null))) + for (t = Wm(t); t !== null; ) { + if ((n = t[$t])) return n; + t = Wm(t); + } + return e; + } + ((t = n), (n = t.parentNode)); + } + return null; + } + function ji(t) { + if ((t = t[$t] || t[bi])) { + var e = t.tag; + if (e === 5 || e === 6 || e === 13 || e === 31 || e === 26 || e === 27 || e === 3) return t; + } + return null; + } + function xa(t) { + var e = t.tag; + if (e === 5 || e === 26 || e === 27 || e === 6) return t.stateNode; + throw Error(r(33)); + } + function Ti(t) { + var e = t[Ef]; + return (e || (e = t[Ef] = { hoistableStyles: new Map(), hoistableScripts: new Map() }), e); + } + function Ft(t) { + t[ga] = !0; + } + var Df = new Set(), + wf = {}; + function $n(t, e) { + (Ai(t, e), Ai(t + 'Capture', e)); + } + function Ai(t, e) { + for (wf[t] = e, t = 0; t < e.length; t++) Df.add(e[t]); + } + var Fg = RegExp( + '^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$' + ), + Lf = {}, + Nf = {}; + function Pg(t) { + return Tr.call(Nf, t) + ? !0 + : Tr.call(Lf, t) + ? !1 + : Fg.test(t) + ? (Nf[t] = !0) + : ((Lf[t] = !0), !1); + } + function Rl(t, e, n) { + if (Pg(e)) + if (n === null) t.removeAttribute(e); + else { + switch (typeof n) { + case 'undefined': + case 'function': + case 'symbol': + t.removeAttribute(e); + return; + case 'boolean': + var a = e.toLowerCase().slice(0, 5); + if (a !== 'data-' && a !== 'aria-') { + t.removeAttribute(e); + return; + } + } + t.setAttribute(e, '' + n); + } + } + function Ol(t, e, n) { + if (n === null) t.removeAttribute(e); + else { + switch (typeof n) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': + t.removeAttribute(e); + return; + } + t.setAttribute(e, '' + n); + } + } + function Pe(t, e, n, a) { + if (a === null) t.removeAttribute(n); + else { + switch (typeof a) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': + t.removeAttribute(n); + return; + } + t.setAttributeNS(e, n, '' + a); + } + } + function Ee(t) { + switch (typeof t) { + case 'bigint': + case 'boolean': + case 'number': + case 'string': + case 'undefined': + return t; + case 'object': + return t; + default: + return ''; + } + } + function _f(t) { + var e = t.type; + return (t = t.nodeName) && t.toLowerCase() === 'input' && (e === 'checkbox' || e === 'radio'); + } + function $g(t, e, n) { + var a = Object.getOwnPropertyDescriptor(t.constructor.prototype, e); + if ( + !t.hasOwnProperty(e) && + typeof a < 'u' && + typeof a.get == 'function' && + typeof a.set == 'function' + ) { + var s = a.get, + u = a.set; + return ( + Object.defineProperty(t, e, { + configurable: !0, + get: function () { + return s.call(this); + }, + set: function (h) { + ((n = '' + h), u.call(this, h)); + }, + }), + Object.defineProperty(t, e, { enumerable: a.enumerable }), + { + getValue: function () { + return n; + }, + setValue: function (h) { + n = '' + h; + }, + stopTracking: function () { + ((t._valueTracker = null), delete t[e]); + }, + } + ); + } + } + function Nr(t) { + if (!t._valueTracker) { + var e = _f(t) ? 'checked' : 'value'; + t._valueTracker = $g(t, e, '' + t[e]); + } + } + function zf(t) { + if (!t) return !1; + var e = t._valueTracker; + if (!e) return !0; + var n = e.getValue(), + a = ''; + return ( + t && (a = _f(t) ? (t.checked ? 'true' : 'false') : t.value), + (t = a), + t !== n ? (e.setValue(t), !0) : !1 + ); + } + function kl(t) { + if (((t = t || (typeof document < 'u' ? document : void 0)), typeof t > 'u')) return null; + try { + return t.activeElement || t.body; + } catch { + return t.body; + } + } + var Ig = /[\n"\\]/g; + function De(t) { + return t.replace(Ig, function (e) { + return '\\' + e.charCodeAt(0).toString(16) + ' '; + }); + } + function _r(t, e, n, a, s, u, h, x) { + ((t.name = ''), + h != null && typeof h != 'function' && typeof h != 'symbol' && typeof h != 'boolean' + ? (t.type = h) + : t.removeAttribute('type'), + e != null + ? h === 'number' + ? ((e === 0 && t.value === '') || t.value != e) && (t.value = '' + Ee(e)) + : t.value !== '' + Ee(e) && (t.value = '' + Ee(e)) + : (h !== 'submit' && h !== 'reset') || t.removeAttribute('value'), + e != null + ? zr(t, h, Ee(e)) + : n != null + ? zr(t, h, Ee(n)) + : a != null && t.removeAttribute('value'), + s == null && u != null && (t.defaultChecked = !!u), + s != null && (t.checked = s && typeof s != 'function' && typeof s != 'symbol'), + x != null && typeof x != 'function' && typeof x != 'symbol' && typeof x != 'boolean' + ? (t.name = '' + Ee(x)) + : t.removeAttribute('name')); + } + function Rf(t, e, n, a, s, u, h, x) { + if ( + (u != null && + typeof u != 'function' && + typeof u != 'symbol' && + typeof u != 'boolean' && + (t.type = u), + e != null || n != null) + ) { + if (!((u !== 'submit' && u !== 'reset') || e != null)) { + Nr(t); + return; + } + ((n = n != null ? '' + Ee(n) : ''), + (e = e != null ? '' + Ee(e) : n), + x || e === t.value || (t.value = e), + (t.defaultValue = e)); + } + ((a = a ?? s), + (a = typeof a != 'function' && typeof a != 'symbol' && !!a), + (t.checked = x ? t.checked : !!a), + (t.defaultChecked = !!a), + h != null && + typeof h != 'function' && + typeof h != 'symbol' && + typeof h != 'boolean' && + (t.name = h), + Nr(t)); + } + function zr(t, e, n) { + (e === 'number' && kl(t.ownerDocument) === t) || + t.defaultValue === '' + n || + (t.defaultValue = '' + n); + } + function Ci(t, e, n, a) { + if (((t = t.options), e)) { + e = {}; + for (var s = 0; s < n.length; s++) e['$' + n[s]] = !0; + for (n = 0; n < t.length; n++) + ((s = e.hasOwnProperty('$' + t[n].value)), + t[n].selected !== s && (t[n].selected = s), + s && a && (t[n].defaultSelected = !0)); + } else { + for (n = '' + Ee(n), e = null, s = 0; s < t.length; s++) { + if (t[s].value === n) { + ((t[s].selected = !0), a && (t[s].defaultSelected = !0)); + return; + } + e !== null || t[s].disabled || (e = t[s]); + } + e !== null && (e.selected = !0); + } + } + function Of(t, e, n) { + if (e != null && ((e = '' + Ee(e)), e !== t.value && (t.value = e), n == null)) { + t.defaultValue !== e && (t.defaultValue = e); + return; + } + t.defaultValue = n != null ? '' + Ee(n) : ''; + } + function kf(t, e, n, a) { + if (e == null) { + if (a != null) { + if (n != null) throw Error(r(92)); + if (Qt(a)) { + if (1 < a.length) throw Error(r(93)); + a = a[0]; + } + n = a; + } + (n == null && (n = ''), (e = n)); + } + ((n = Ee(e)), + (t.defaultValue = n), + (a = t.textContent), + a === n && a !== '' && a !== null && (t.value = a), + Nr(t)); + } + function Mi(t, e) { + if (e) { + var n = t.firstChild; + if (n && n === t.lastChild && n.nodeType === 3) { + n.nodeValue = e; + return; + } + } + t.textContent = e; + } + var t1 = new Set( + 'animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp'.split( + ' ' + ) + ); + function Vf(t, e, n) { + var a = e.indexOf('--') === 0; + n == null || typeof n == 'boolean' || n === '' + ? a + ? t.setProperty(e, '') + : e === 'float' + ? (t.cssFloat = '') + : (t[e] = '') + : a + ? t.setProperty(e, n) + : typeof n != 'number' || n === 0 || t1.has(e) + ? e === 'float' + ? (t.cssFloat = n) + : (t[e] = ('' + n).trim()) + : (t[e] = n + 'px'); + } + function Bf(t, e, n) { + if (e != null && typeof e != 'object') throw Error(r(62)); + if (((t = t.style), n != null)) { + for (var a in n) + !n.hasOwnProperty(a) || + (e != null && e.hasOwnProperty(a)) || + (a.indexOf('--') === 0 + ? t.setProperty(a, '') + : a === 'float' + ? (t.cssFloat = '') + : (t[a] = '')); + for (var s in e) ((a = e[s]), e.hasOwnProperty(s) && n[s] !== a && Vf(t, s, a)); + } else for (var u in e) e.hasOwnProperty(u) && Vf(t, u, e[u]); + } + function Rr(t) { + if (t.indexOf('-') === -1) return !1; + switch (t) { + case 'annotation-xml': + case 'color-profile': + case 'font-face': + case 'font-face-src': + case 'font-face-uri': + case 'font-face-format': + case 'font-face-name': + case 'missing-glyph': + return !1; + default: + return !0; + } + } + var e1 = new Map([ + ['acceptCharset', 'accept-charset'], + ['htmlFor', 'for'], + ['httpEquiv', 'http-equiv'], + ['crossOrigin', 'crossorigin'], + ['accentHeight', 'accent-height'], + ['alignmentBaseline', 'alignment-baseline'], + ['arabicForm', 'arabic-form'], + ['baselineShift', 'baseline-shift'], + ['capHeight', 'cap-height'], + ['clipPath', 'clip-path'], + ['clipRule', 'clip-rule'], + ['colorInterpolation', 'color-interpolation'], + ['colorInterpolationFilters', 'color-interpolation-filters'], + ['colorProfile', 'color-profile'], + ['colorRendering', 'color-rendering'], + ['dominantBaseline', 'dominant-baseline'], + ['enableBackground', 'enable-background'], + ['fillOpacity', 'fill-opacity'], + ['fillRule', 'fill-rule'], + ['floodColor', 'flood-color'], + ['floodOpacity', 'flood-opacity'], + ['fontFamily', 'font-family'], + ['fontSize', 'font-size'], + ['fontSizeAdjust', 'font-size-adjust'], + ['fontStretch', 'font-stretch'], + ['fontStyle', 'font-style'], + ['fontVariant', 'font-variant'], + ['fontWeight', 'font-weight'], + ['glyphName', 'glyph-name'], + ['glyphOrientationHorizontal', 'glyph-orientation-horizontal'], + ['glyphOrientationVertical', 'glyph-orientation-vertical'], + ['horizAdvX', 'horiz-adv-x'], + ['horizOriginX', 'horiz-origin-x'], + ['imageRendering', 'image-rendering'], + ['letterSpacing', 'letter-spacing'], + ['lightingColor', 'lighting-color'], + ['markerEnd', 'marker-end'], + ['markerMid', 'marker-mid'], + ['markerStart', 'marker-start'], + ['overlinePosition', 'overline-position'], + ['overlineThickness', 'overline-thickness'], + ['paintOrder', 'paint-order'], + ['panose-1', 'panose-1'], + ['pointerEvents', 'pointer-events'], + ['renderingIntent', 'rendering-intent'], + ['shapeRendering', 'shape-rendering'], + ['stopColor', 'stop-color'], + ['stopOpacity', 'stop-opacity'], + ['strikethroughPosition', 'strikethrough-position'], + ['strikethroughThickness', 'strikethrough-thickness'], + ['strokeDasharray', 'stroke-dasharray'], + ['strokeDashoffset', 'stroke-dashoffset'], + ['strokeLinecap', 'stroke-linecap'], + ['strokeLinejoin', 'stroke-linejoin'], + ['strokeMiterlimit', 'stroke-miterlimit'], + ['strokeOpacity', 'stroke-opacity'], + ['strokeWidth', 'stroke-width'], + ['textAnchor', 'text-anchor'], + ['textDecoration', 'text-decoration'], + ['textRendering', 'text-rendering'], + ['transformOrigin', 'transform-origin'], + ['underlinePosition', 'underline-position'], + ['underlineThickness', 'underline-thickness'], + ['unicodeBidi', 'unicode-bidi'], + ['unicodeRange', 'unicode-range'], + ['unitsPerEm', 'units-per-em'], + ['vAlphabetic', 'v-alphabetic'], + ['vHanging', 'v-hanging'], + ['vIdeographic', 'v-ideographic'], + ['vMathematical', 'v-mathematical'], + ['vectorEffect', 'vector-effect'], + ['vertAdvY', 'vert-adv-y'], + ['vertOriginX', 'vert-origin-x'], + ['vertOriginY', 'vert-origin-y'], + ['wordSpacing', 'word-spacing'], + ['writingMode', 'writing-mode'], + ['xmlnsXlink', 'xmlns:xlink'], + ['xHeight', 'x-height'], + ]), + n1 = + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i; + function Vl(t) { + return n1.test('' + t) + ? "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')" + : t; + } + function $e() {} + var Or = null; + function kr(t) { + return ( + (t = t.target || t.srcElement || window), + t.correspondingUseElement && (t = t.correspondingUseElement), + t.nodeType === 3 ? t.parentNode : t + ); + } + var Ei = null, + Di = null; + function Uf(t) { + var e = ji(t); + if (e && (t = e.stateNode)) { + var n = t[ce] || null; + t: switch (((t = e.stateNode), e.type)) { + case 'input': + if ( + (_r( + t, + n.value, + n.defaultValue, + n.defaultValue, + n.checked, + n.defaultChecked, + n.type, + n.name + ), + (e = n.name), + n.type === 'radio' && e != null) + ) { + for (n = t; n.parentNode; ) n = n.parentNode; + for ( + n = n.querySelectorAll('input[name="' + De('' + e) + '"][type="radio"]'), e = 0; + e < n.length; + e++ + ) { + var a = n[e]; + if (a !== t && a.form === t.form) { + var s = a[ce] || null; + if (!s) throw Error(r(90)); + _r( + a, + s.value, + s.defaultValue, + s.defaultValue, + s.checked, + s.defaultChecked, + s.type, + s.name + ); + } + } + for (e = 0; e < n.length; e++) ((a = n[e]), a.form === t.form && zf(a)); + } + break t; + case 'textarea': + Of(t, n.value, n.defaultValue); + break t; + case 'select': + ((e = n.value), e != null && Ci(t, !!n.multiple, e, !1)); + } + } + } + var Vr = !1; + function Hf(t, e, n) { + if (Vr) return t(e, n); + Vr = !0; + try { + var a = t(e); + return a; + } finally { + if ( + ((Vr = !1), + (Ei !== null || Di !== null) && + (As(), Ei && ((e = Ei), (t = Di), (Di = Ei = null), Uf(e), t))) + ) + for (e = 0; e < t.length; e++) Uf(t[e]); + } + } + function va(t, e) { + var n = t.stateNode; + if (n === null) return null; + var a = n[ce] || null; + if (a === null) return null; + n = a[e]; + t: switch (e) { + case 'onClick': + case 'onClickCapture': + case 'onDoubleClick': + case 'onDoubleClickCapture': + case 'onMouseDown': + case 'onMouseDownCapture': + case 'onMouseMove': + case 'onMouseMoveCapture': + case 'onMouseUp': + case 'onMouseUpCapture': + case 'onMouseEnter': + ((a = !a.disabled) || + ((t = t.type), + (a = !(t === 'button' || t === 'input' || t === 'select' || t === 'textarea'))), + (t = !a)); + break t; + default: + t = !1; + } + if (t) return null; + if (n && typeof n != 'function') throw Error(r(231, e, typeof n)); + return n; + } + var Ie = !( + typeof window > 'u' || + typeof window.document > 'u' || + typeof window.document.createElement > 'u' + ), + Br = !1; + if (Ie) + try { + var ba = {}; + (Object.defineProperty(ba, 'passive', { + get: function () { + Br = !0; + }, + }), + window.addEventListener('test', ba, ba), + window.removeEventListener('test', ba, ba)); + } catch { + Br = !1; + } + var bn = null, + Ur = null, + Bl = null; + function Yf() { + if (Bl) return Bl; + var t, + e = Ur, + n = e.length, + a, + s = 'value' in bn ? bn.value : bn.textContent, + u = s.length; + for (t = 0; t < n && e[t] === s[t]; t++); + var h = n - t; + for (a = 1; a <= h && e[n - a] === s[u - a]; a++); + return (Bl = s.slice(t, 1 < a ? 1 - a : void 0)); + } + function Ul(t) { + var e = t.keyCode; + return ( + 'charCode' in t ? ((t = t.charCode), t === 0 && e === 13 && (t = 13)) : (t = e), + t === 10 && (t = 13), + 32 <= t || t === 13 ? t : 0 + ); + } + function Hl() { + return !0; + } + function qf() { + return !1; + } + function fe(t) { + function e(n, a, s, u, h) { + ((this._reactName = n), + (this._targetInst = s), + (this.type = a), + (this.nativeEvent = u), + (this.target = h), + (this.currentTarget = null)); + for (var x in t) t.hasOwnProperty(x) && ((n = t[x]), (this[x] = n ? n(u) : u[x])); + return ( + (this.isDefaultPrevented = ( + u.defaultPrevented != null ? u.defaultPrevented : u.returnValue === !1 + ) + ? Hl + : qf), + (this.isPropagationStopped = qf), + this + ); + } + return ( + b(e.prototype, { + preventDefault: function () { + this.defaultPrevented = !0; + var n = this.nativeEvent; + n && + (n.preventDefault + ? n.preventDefault() + : typeof n.returnValue != 'unknown' && (n.returnValue = !1), + (this.isDefaultPrevented = Hl)); + }, + stopPropagation: function () { + var n = this.nativeEvent; + n && + (n.stopPropagation + ? n.stopPropagation() + : typeof n.cancelBubble != 'unknown' && (n.cancelBubble = !0), + (this.isPropagationStopped = Hl)); + }, + persist: function () {}, + isPersistent: Hl, + }), + e + ); + } + var In = { + eventPhase: 0, + bubbles: 0, + cancelable: 0, + timeStamp: function (t) { + return t.timeStamp || Date.now(); + }, + defaultPrevented: 0, + isTrusted: 0, + }, + Yl = fe(In), + Sa = b({}, In, { view: 0, detail: 0 }), + i1 = fe(Sa), + Hr, + Yr, + ja, + ql = b({}, Sa, { + screenX: 0, + screenY: 0, + clientX: 0, + clientY: 0, + pageX: 0, + pageY: 0, + ctrlKey: 0, + shiftKey: 0, + altKey: 0, + metaKey: 0, + getModifierState: Gr, + button: 0, + buttons: 0, + relatedTarget: function (t) { + return t.relatedTarget === void 0 + ? t.fromElement === t.srcElement + ? t.toElement + : t.fromElement + : t.relatedTarget; + }, + movementX: function (t) { + return 'movementX' in t + ? t.movementX + : (t !== ja && + (ja && t.type === 'mousemove' + ? ((Hr = t.screenX - ja.screenX), (Yr = t.screenY - ja.screenY)) + : (Yr = Hr = 0), + (ja = t)), + Hr); + }, + movementY: function (t) { + return 'movementY' in t ? t.movementY : Yr; + }, + }), + Gf = fe(ql), + a1 = b({}, ql, { dataTransfer: 0 }), + l1 = fe(a1), + s1 = b({}, Sa, { relatedTarget: 0 }), + qr = fe(s1), + r1 = b({}, In, { animationName: 0, elapsedTime: 0, pseudoElement: 0 }), + o1 = fe(r1), + u1 = b({}, In, { + clipboardData: function (t) { + return 'clipboardData' in t ? t.clipboardData : window.clipboardData; + }, + }), + c1 = fe(u1), + f1 = b({}, In, { data: 0 }), + Xf = fe(f1), + d1 = { + Esc: 'Escape', + Spacebar: ' ', + Left: 'ArrowLeft', + Up: 'ArrowUp', + Right: 'ArrowRight', + Down: 'ArrowDown', + Del: 'Delete', + Win: 'OS', + Menu: 'ContextMenu', + Apps: 'ContextMenu', + Scroll: 'ScrollLock', + MozPrintableKey: 'Unidentified', + }, + h1 = { + 8: 'Backspace', + 9: 'Tab', + 12: 'Clear', + 13: 'Enter', + 16: 'Shift', + 17: 'Control', + 18: 'Alt', + 19: 'Pause', + 20: 'CapsLock', + 27: 'Escape', + 32: ' ', + 33: 'PageUp', + 34: 'PageDown', + 35: 'End', + 36: 'Home', + 37: 'ArrowLeft', + 38: 'ArrowUp', + 39: 'ArrowRight', + 40: 'ArrowDown', + 45: 'Insert', + 46: 'Delete', + 112: 'F1', + 113: 'F2', + 114: 'F3', + 115: 'F4', + 116: 'F5', + 117: 'F6', + 118: 'F7', + 119: 'F8', + 120: 'F9', + 121: 'F10', + 122: 'F11', + 123: 'F12', + 144: 'NumLock', + 145: 'ScrollLock', + 224: 'Meta', + }, + m1 = { Alt: 'altKey', Control: 'ctrlKey', Meta: 'metaKey', Shift: 'shiftKey' }; + function p1(t) { + var e = this.nativeEvent; + return e.getModifierState ? e.getModifierState(t) : (t = m1[t]) ? !!e[t] : !1; + } + function Gr() { + return p1; + } + var y1 = b({}, Sa, { + key: function (t) { + if (t.key) { + var e = d1[t.key] || t.key; + if (e !== 'Unidentified') return e; + } + return t.type === 'keypress' + ? ((t = Ul(t)), t === 13 ? 'Enter' : String.fromCharCode(t)) + : t.type === 'keydown' || t.type === 'keyup' + ? h1[t.keyCode] || 'Unidentified' + : ''; + }, + code: 0, + location: 0, + ctrlKey: 0, + shiftKey: 0, + altKey: 0, + metaKey: 0, + repeat: 0, + locale: 0, + getModifierState: Gr, + charCode: function (t) { + return t.type === 'keypress' ? Ul(t) : 0; + }, + keyCode: function (t) { + return t.type === 'keydown' || t.type === 'keyup' ? t.keyCode : 0; + }, + which: function (t) { + return t.type === 'keypress' + ? Ul(t) + : t.type === 'keydown' || t.type === 'keyup' + ? t.keyCode + : 0; + }, + }), + g1 = fe(y1), + x1 = b({}, ql, { + pointerId: 0, + width: 0, + height: 0, + pressure: 0, + tangentialPressure: 0, + tiltX: 0, + tiltY: 0, + twist: 0, + pointerType: 0, + isPrimary: 0, + }), + Zf = fe(x1), + v1 = b({}, Sa, { + touches: 0, + targetTouches: 0, + changedTouches: 0, + altKey: 0, + metaKey: 0, + ctrlKey: 0, + shiftKey: 0, + getModifierState: Gr, + }), + b1 = fe(v1), + S1 = b({}, In, { propertyName: 0, elapsedTime: 0, pseudoElement: 0 }), + j1 = fe(S1), + T1 = b({}, ql, { + deltaX: function (t) { + return 'deltaX' in t ? t.deltaX : 'wheelDeltaX' in t ? -t.wheelDeltaX : 0; + }, + deltaY: function (t) { + return 'deltaY' in t + ? t.deltaY + : 'wheelDeltaY' in t + ? -t.wheelDeltaY + : 'wheelDelta' in t + ? -t.wheelDelta + : 0; + }, + deltaZ: 0, + deltaMode: 0, + }), + A1 = fe(T1), + C1 = b({}, In, { newState: 0, oldState: 0 }), + M1 = fe(C1), + E1 = [9, 13, 27, 32], + Xr = Ie && 'CompositionEvent' in window, + Ta = null; + Ie && 'documentMode' in document && (Ta = document.documentMode); + var D1 = Ie && 'TextEvent' in window && !Ta, + Qf = Ie && (!Xr || (Ta && 8 < Ta && 11 >= Ta)), + Kf = ' ', + Wf = !1; + function Jf(t, e) { + switch (t) { + case 'keyup': + return E1.indexOf(e.keyCode) !== -1; + case 'keydown': + return e.keyCode !== 229; + case 'keypress': + case 'mousedown': + case 'focusout': + return !0; + default: + return !1; + } + } + function Ff(t) { + return ((t = t.detail), typeof t == 'object' && 'data' in t ? t.data : null); + } + var wi = !1; + function w1(t, e) { + switch (t) { + case 'compositionend': + return Ff(e); + case 'keypress': + return e.which !== 32 ? null : ((Wf = !0), Kf); + case 'textInput': + return ((t = e.data), t === Kf && Wf ? null : t); + default: + return null; + } + } + function L1(t, e) { + if (wi) + return t === 'compositionend' || (!Xr && Jf(t, e)) + ? ((t = Yf()), (Bl = Ur = bn = null), (wi = !1), t) + : null; + switch (t) { + case 'paste': + return null; + case 'keypress': + if (!(e.ctrlKey || e.altKey || e.metaKey) || (e.ctrlKey && e.altKey)) { + if (e.char && 1 < e.char.length) return e.char; + if (e.which) return String.fromCharCode(e.which); + } + return null; + case 'compositionend': + return Qf && e.locale !== 'ko' ? null : e.data; + default: + return null; + } + } + var N1 = { + color: !0, + date: !0, + datetime: !0, + 'datetime-local': !0, + email: !0, + month: !0, + number: !0, + password: !0, + range: !0, + search: !0, + tel: !0, + text: !0, + time: !0, + url: !0, + week: !0, + }; + function Pf(t) { + var e = t && t.nodeName && t.nodeName.toLowerCase(); + return e === 'input' ? !!N1[t.type] : e === 'textarea'; + } + function $f(t, e, n, a) { + (Ei ? (Di ? Di.push(a) : (Di = [a])) : (Ei = a), + (e = Ns(e, 'onChange')), + 0 < e.length && + ((n = new Yl('onChange', 'change', null, n, a)), t.push({ event: n, listeners: e }))); + } + var Aa = null, + Ca = null; + function _1(t) { + Rm(t, 0); + } + function Gl(t) { + var e = xa(t); + if (zf(e)) return t; + } + function If(t, e) { + if (t === 'change') return e; + } + var td = !1; + if (Ie) { + var Zr; + if (Ie) { + var Qr = 'oninput' in document; + if (!Qr) { + var ed = document.createElement('div'); + (ed.setAttribute('oninput', 'return;'), (Qr = typeof ed.oninput == 'function')); + } + Zr = Qr; + } else Zr = !1; + td = Zr && (!document.documentMode || 9 < document.documentMode); + } + function nd() { + Aa && (Aa.detachEvent('onpropertychange', id), (Ca = Aa = null)); + } + function id(t) { + if (t.propertyName === 'value' && Gl(Ca)) { + var e = []; + ($f(e, Ca, t, kr(t)), Hf(_1, e)); + } + } + function z1(t, e, n) { + t === 'focusin' + ? (nd(), (Aa = e), (Ca = n), Aa.attachEvent('onpropertychange', id)) + : t === 'focusout' && nd(); + } + function R1(t) { + if (t === 'selectionchange' || t === 'keyup' || t === 'keydown') return Gl(Ca); + } + function O1(t, e) { + if (t === 'click') return Gl(e); + } + function k1(t, e) { + if (t === 'input' || t === 'change') return Gl(e); + } + function V1(t, e) { + return (t === e && (t !== 0 || 1 / t === 1 / e)) || (t !== t && e !== e); + } + var be = typeof Object.is == 'function' ? Object.is : V1; + function Ma(t, e) { + if (be(t, e)) return !0; + if (typeof t != 'object' || t === null || typeof e != 'object' || e === null) return !1; + var n = Object.keys(t), + a = Object.keys(e); + if (n.length !== a.length) return !1; + for (a = 0; a < n.length; a++) { + var s = n[a]; + if (!Tr.call(e, s) || !be(t[s], e[s])) return !1; + } + return !0; + } + function ad(t) { + for (; t && t.firstChild; ) t = t.firstChild; + return t; + } + function ld(t, e) { + var n = ad(t); + t = 0; + for (var a; n; ) { + if (n.nodeType === 3) { + if (((a = t + n.textContent.length), t <= e && a >= e)) return { node: n, offset: e - t }; + t = a; + } + t: { + for (; n; ) { + if (n.nextSibling) { + n = n.nextSibling; + break t; + } + n = n.parentNode; + } + n = void 0; + } + n = ad(n); + } + } + function sd(t, e) { + return t && e + ? t === e + ? !0 + : t && t.nodeType === 3 + ? !1 + : e && e.nodeType === 3 + ? sd(t, e.parentNode) + : 'contains' in t + ? t.contains(e) + : t.compareDocumentPosition + ? !!(t.compareDocumentPosition(e) & 16) + : !1 + : !1; + } + function rd(t) { + t = + t != null && t.ownerDocument != null && t.ownerDocument.defaultView != null + ? t.ownerDocument.defaultView + : window; + for (var e = kl(t.document); e instanceof t.HTMLIFrameElement; ) { + try { + var n = typeof e.contentWindow.location.href == 'string'; + } catch { + n = !1; + } + if (n) t = e.contentWindow; + else break; + e = kl(t.document); + } + return e; + } + function Kr(t) { + var e = t && t.nodeName && t.nodeName.toLowerCase(); + return ( + e && + ((e === 'input' && + (t.type === 'text' || + t.type === 'search' || + t.type === 'tel' || + t.type === 'url' || + t.type === 'password')) || + e === 'textarea' || + t.contentEditable === 'true') + ); + } + var B1 = Ie && 'documentMode' in document && 11 >= document.documentMode, + Li = null, + Wr = null, + Ea = null, + Jr = !1; + function od(t, e, n) { + var a = n.window === n ? n.document : n.nodeType === 9 ? n : n.ownerDocument; + Jr || + Li == null || + Li !== kl(a) || + ((a = Li), + 'selectionStart' in a && Kr(a) + ? (a = { start: a.selectionStart, end: a.selectionEnd }) + : ((a = ((a.ownerDocument && a.ownerDocument.defaultView) || window).getSelection()), + (a = { + anchorNode: a.anchorNode, + anchorOffset: a.anchorOffset, + focusNode: a.focusNode, + focusOffset: a.focusOffset, + })), + (Ea && Ma(Ea, a)) || + ((Ea = a), + (a = Ns(Wr, 'onSelect')), + 0 < a.length && + ((e = new Yl('onSelect', 'select', null, e, n)), + t.push({ event: e, listeners: a }), + (e.target = Li)))); + } + function ti(t, e) { + var n = {}; + return ( + (n[t.toLowerCase()] = e.toLowerCase()), + (n['Webkit' + t] = 'webkit' + e), + (n['Moz' + t] = 'moz' + e), + n + ); + } + var Ni = { + animationend: ti('Animation', 'AnimationEnd'), + animationiteration: ti('Animation', 'AnimationIteration'), + animationstart: ti('Animation', 'AnimationStart'), + transitionrun: ti('Transition', 'TransitionRun'), + transitionstart: ti('Transition', 'TransitionStart'), + transitioncancel: ti('Transition', 'TransitionCancel'), + transitionend: ti('Transition', 'TransitionEnd'), + }, + Fr = {}, + ud = {}; + Ie && + ((ud = document.createElement('div').style), + 'AnimationEvent' in window || + (delete Ni.animationend.animation, + delete Ni.animationiteration.animation, + delete Ni.animationstart.animation), + 'TransitionEvent' in window || delete Ni.transitionend.transition); + function ei(t) { + if (Fr[t]) return Fr[t]; + if (!Ni[t]) return t; + var e = Ni[t], + n; + for (n in e) if (e.hasOwnProperty(n) && n in ud) return (Fr[t] = e[n]); + return t; + } + var cd = ei('animationend'), + fd = ei('animationiteration'), + dd = ei('animationstart'), + U1 = ei('transitionrun'), + H1 = ei('transitionstart'), + Y1 = ei('transitioncancel'), + hd = ei('transitionend'), + md = new Map(), + Pr = + 'abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel'.split( + ' ' + ); + Pr.push('scrollEnd'); + function He(t, e) { + (md.set(t, e), $n(e, [t])); + } + var Xl = + typeof reportError == 'function' + ? reportError + : function (t) { + if (typeof window == 'object' && typeof window.ErrorEvent == 'function') { + var e = new window.ErrorEvent('error', { + bubbles: !0, + cancelable: !0, + message: + typeof t == 'object' && t !== null && typeof t.message == 'string' + ? String(t.message) + : String(t), + error: t, + }); + if (!window.dispatchEvent(e)) return; + } else if (typeof process == 'object' && typeof process.emit == 'function') { + process.emit('uncaughtException', t); + return; + } + console.error(t); + }, + we = [], + _i = 0, + $r = 0; + function Zl() { + for (var t = _i, e = ($r = _i = 0); e < t; ) { + var n = we[e]; + we[e++] = null; + var a = we[e]; + we[e++] = null; + var s = we[e]; + we[e++] = null; + var u = we[e]; + if (((we[e++] = null), a !== null && s !== null)) { + var h = a.pending; + (h === null ? (s.next = s) : ((s.next = h.next), (h.next = s)), (a.pending = s)); + } + u !== 0 && pd(n, s, u); + } + } + function Ql(t, e, n, a) { + ((we[_i++] = t), + (we[_i++] = e), + (we[_i++] = n), + (we[_i++] = a), + ($r |= a), + (t.lanes |= a), + (t = t.alternate), + t !== null && (t.lanes |= a)); + } + function Ir(t, e, n, a) { + return (Ql(t, e, n, a), Kl(t)); + } + function ni(t, e) { + return (Ql(t, null, null, e), Kl(t)); + } + function pd(t, e, n) { + t.lanes |= n; + var a = t.alternate; + a !== null && (a.lanes |= n); + for (var s = !1, u = t.return; u !== null; ) + ((u.childLanes |= n), + (a = u.alternate), + a !== null && (a.childLanes |= n), + u.tag === 22 && ((t = u.stateNode), t === null || t._visibility & 1 || (s = !0)), + (t = u), + (u = u.return)); + return t.tag === 3 + ? ((u = t.stateNode), + s && + e !== null && + ((s = 31 - ve(n)), + (t = u.hiddenUpdates), + (a = t[s]), + a === null ? (t[s] = [e]) : a.push(e), + (e.lane = n | 536870912)), + u) + : null; + } + function Kl(t) { + if (50 < Ja) throw ((Ja = 0), (uu = null), Error(r(185))); + for (var e = t.return; e !== null; ) ((t = e), (e = t.return)); + return t.tag === 3 ? t.stateNode : null; + } + var zi = {}; + function q1(t, e, n, a) { + ((this.tag = t), + (this.key = n), + (this.sibling = + this.child = + this.return = + this.stateNode = + this.type = + this.elementType = + null), + (this.index = 0), + (this.refCleanup = this.ref = null), + (this.pendingProps = e), + (this.dependencies = this.memoizedState = this.updateQueue = this.memoizedProps = null), + (this.mode = a), + (this.subtreeFlags = this.flags = 0), + (this.deletions = null), + (this.childLanes = this.lanes = 0), + (this.alternate = null)); + } + function Se(t, e, n, a) { + return new q1(t, e, n, a); + } + function to(t) { + return ((t = t.prototype), !(!t || !t.isReactComponent)); + } + function tn(t, e) { + var n = t.alternate; + return ( + n === null + ? ((n = Se(t.tag, e, t.key, t.mode)), + (n.elementType = t.elementType), + (n.type = t.type), + (n.stateNode = t.stateNode), + (n.alternate = t), + (t.alternate = n)) + : ((n.pendingProps = e), + (n.type = t.type), + (n.flags = 0), + (n.subtreeFlags = 0), + (n.deletions = null)), + (n.flags = t.flags & 65011712), + (n.childLanes = t.childLanes), + (n.lanes = t.lanes), + (n.child = t.child), + (n.memoizedProps = t.memoizedProps), + (n.memoizedState = t.memoizedState), + (n.updateQueue = t.updateQueue), + (e = t.dependencies), + (n.dependencies = e === null ? null : { lanes: e.lanes, firstContext: e.firstContext }), + (n.sibling = t.sibling), + (n.index = t.index), + (n.ref = t.ref), + (n.refCleanup = t.refCleanup), + n + ); + } + function yd(t, e) { + t.flags &= 65011714; + var n = t.alternate; + return ( + n === null + ? ((t.childLanes = 0), + (t.lanes = e), + (t.child = null), + (t.subtreeFlags = 0), + (t.memoizedProps = null), + (t.memoizedState = null), + (t.updateQueue = null), + (t.dependencies = null), + (t.stateNode = null)) + : ((t.childLanes = n.childLanes), + (t.lanes = n.lanes), + (t.child = n.child), + (t.subtreeFlags = 0), + (t.deletions = null), + (t.memoizedProps = n.memoizedProps), + (t.memoizedState = n.memoizedState), + (t.updateQueue = n.updateQueue), + (t.type = n.type), + (e = n.dependencies), + (t.dependencies = e === null ? null : { lanes: e.lanes, firstContext: e.firstContext })), + t + ); + } + function Wl(t, e, n, a, s, u) { + var h = 0; + if (((a = t), typeof t == 'function')) to(t) && (h = 1); + else if (typeof t == 'string') + h = Kx(t, n, $.current) ? 26 : t === 'html' || t === 'head' || t === 'body' ? 27 : 5; + else + t: switch (t) { + case it: + return ((t = Se(31, n, e, s)), (t.elementType = it), (t.lanes = u), t); + case H: + return ii(n.children, s, u, e); + case G: + ((h = 8), (s |= 24)); + break; + case U: + return ((t = Se(12, n, e, s | 2)), (t.elementType = U), (t.lanes = u), t); + case Q: + return ((t = Se(13, n, e, s)), (t.elementType = Q), (t.lanes = u), t); + case nt: + return ((t = Se(19, n, e, s)), (t.elementType = nt), (t.lanes = u), t); + default: + if (typeof t == 'object' && t !== null) + switch (t.$$typeof) { + case V: + h = 10; + break t; + case q: + h = 9; + break t; + case Z: + h = 11; + break t; + case F: + h = 14; + break t; + case K: + ((h = 16), (a = null)); + break t; + } + ((h = 29), (n = Error(r(130, t === null ? 'null' : typeof t, ''))), (a = null)); + } + return ((e = Se(h, n, e, s)), (e.elementType = t), (e.type = a), (e.lanes = u), e); + } + function ii(t, e, n, a) { + return ((t = Se(7, t, a, e)), (t.lanes = n), t); + } + function eo(t, e, n) { + return ((t = Se(6, t, null, e)), (t.lanes = n), t); + } + function gd(t) { + var e = Se(18, null, null, 0); + return ((e.stateNode = t), e); + } + function no(t, e, n) { + return ( + (e = Se(4, t.children !== null ? t.children : [], t.key, e)), + (e.lanes = n), + (e.stateNode = { + containerInfo: t.containerInfo, + pendingChildren: null, + implementation: t.implementation, + }), + e + ); + } + var xd = new WeakMap(); + function Le(t, e) { + if (typeof t == 'object' && t !== null) { + var n = xd.get(t); + return n !== void 0 ? n : ((e = { value: t, source: e, stack: gf(e) }), xd.set(t, e), e); + } + return { value: t, source: e, stack: gf(e) }; + } + var Ri = [], + Oi = 0, + Jl = null, + Da = 0, + Ne = [], + _e = 0, + Sn = null, + Ze = 1, + Qe = ''; + function en(t, e) { + ((Ri[Oi++] = Da), (Ri[Oi++] = Jl), (Jl = t), (Da = e)); + } + function vd(t, e, n) { + ((Ne[_e++] = Ze), (Ne[_e++] = Qe), (Ne[_e++] = Sn), (Sn = t)); + var a = Ze; + t = Qe; + var s = 32 - ve(a) - 1; + ((a &= ~(1 << s)), (n += 1)); + var u = 32 - ve(e) + s; + if (30 < u) { + var h = s - (s % 5); + ((u = (a & ((1 << h) - 1)).toString(32)), + (a >>= h), + (s -= h), + (Ze = (1 << (32 - ve(e) + s)) | (n << s) | a), + (Qe = u + t)); + } else ((Ze = (1 << u) | (n << s) | a), (Qe = t)); + } + function io(t) { + t.return !== null && (en(t, 1), vd(t, 1, 0)); + } + function ao(t) { + for (; t === Jl; ) ((Jl = Ri[--Oi]), (Ri[Oi] = null), (Da = Ri[--Oi]), (Ri[Oi] = null)); + for (; t === Sn; ) + ((Sn = Ne[--_e]), + (Ne[_e] = null), + (Qe = Ne[--_e]), + (Ne[_e] = null), + (Ze = Ne[--_e]), + (Ne[_e] = null)); + } + function bd(t, e) { + ((Ne[_e++] = Ze), (Ne[_e++] = Qe), (Ne[_e++] = Sn), (Ze = e.id), (Qe = e.overflow), (Sn = t)); + } + var It = null, + wt = null, + pt = !1, + jn = null, + ze = !1, + lo = Error(r(519)); + function Tn(t) { + var e = Error( + r(418, 1 < arguments.length && arguments[1] !== void 0 && arguments[1] ? 'text' : 'HTML', '') + ); + throw (wa(Le(e, t)), lo); + } + function Sd(t) { + var e = t.stateNode, + n = t.type, + a = t.memoizedProps; + switch (((e[$t] = t), (e[ce] = a), n)) { + case 'dialog': + (ft('cancel', e), ft('close', e)); + break; + case 'iframe': + case 'object': + case 'embed': + ft('load', e); + break; + case 'video': + case 'audio': + for (n = 0; n < Pa.length; n++) ft(Pa[n], e); + break; + case 'source': + ft('error', e); + break; + case 'img': + case 'image': + case 'link': + (ft('error', e), ft('load', e)); + break; + case 'details': + ft('toggle', e); + break; + case 'input': + (ft('invalid', e), + Rf(e, a.value, a.defaultValue, a.checked, a.defaultChecked, a.type, a.name, !0)); + break; + case 'select': + ft('invalid', e); + break; + case 'textarea': + (ft('invalid', e), kf(e, a.value, a.defaultValue, a.children)); + } + ((n = a.children), + (typeof n != 'string' && typeof n != 'number' && typeof n != 'bigint') || + e.textContent === '' + n || + a.suppressHydrationWarning === !0 || + Bm(e.textContent, n) + ? (a.popover != null && (ft('beforetoggle', e), ft('toggle', e)), + a.onScroll != null && ft('scroll', e), + a.onScrollEnd != null && ft('scrollend', e), + a.onClick != null && (e.onclick = $e), + (e = !0)) + : (e = !1), + e || Tn(t, !0)); + } + function jd(t) { + for (It = t.return; It; ) + switch (It.tag) { + case 5: + case 31: + case 13: + ze = !1; + return; + case 27: + case 3: + ze = !0; + return; + default: + It = It.return; + } + } + function ki(t) { + if (t !== It) return !1; + if (!pt) return (jd(t), (pt = !0), !1); + var e = t.tag, + n; + if ( + ((n = e !== 3 && e !== 27) && + ((n = e === 5) && + ((n = t.type), (n = !(n !== 'form' && n !== 'button') || Au(t.type, t.memoizedProps))), + (n = !n)), + n && wt && Tn(t), + jd(t), + e === 13) + ) { + if (((t = t.memoizedState), (t = t !== null ? t.dehydrated : null), !t)) throw Error(r(317)); + wt = Km(t); + } else if (e === 31) { + if (((t = t.memoizedState), (t = t !== null ? t.dehydrated : null), !t)) throw Error(r(317)); + wt = Km(t); + } else + e === 27 + ? ((e = wt), Vn(t.type) ? ((t = wu), (wu = null), (wt = t)) : (wt = e)) + : (wt = It ? Oe(t.stateNode.nextSibling) : null); + return !0; + } + function ai() { + ((wt = It = null), (pt = !1)); + } + function so() { + var t = jn; + return (t !== null && (pe === null ? (pe = t) : pe.push.apply(pe, t), (jn = null)), t); + } + function wa(t) { + jn === null ? (jn = [t]) : jn.push(t); + } + var ro = A(null), + li = null, + nn = null; + function An(t, e, n) { + (X(ro, e._currentValue), (e._currentValue = n)); + } + function an(t) { + ((t._currentValue = ro.current), k(ro)); + } + function oo(t, e, n) { + for (; t !== null; ) { + var a = t.alternate; + if ( + ((t.childLanes & e) !== e + ? ((t.childLanes |= e), a !== null && (a.childLanes |= e)) + : a !== null && (a.childLanes & e) !== e && (a.childLanes |= e), + t === n) + ) + break; + t = t.return; + } + } + function uo(t, e, n, a) { + var s = t.child; + for (s !== null && (s.return = t); s !== null; ) { + var u = s.dependencies; + if (u !== null) { + var h = s.child; + u = u.firstContext; + t: for (; u !== null; ) { + var x = u; + u = s; + for (var S = 0; S < e.length; S++) + if (x.context === e[S]) { + ((u.lanes |= n), + (x = u.alternate), + x !== null && (x.lanes |= n), + oo(u.return, n, t), + a || (h = null)); + break t; + } + u = x.next; + } + } else if (s.tag === 18) { + if (((h = s.return), h === null)) throw Error(r(341)); + ((h.lanes |= n), (u = h.alternate), u !== null && (u.lanes |= n), oo(h, n, t), (h = null)); + } else h = s.child; + if (h !== null) h.return = s; + else + for (h = s; h !== null; ) { + if (h === t) { + h = null; + break; + } + if (((s = h.sibling), s !== null)) { + ((s.return = h.return), (h = s)); + break; + } + h = h.return; + } + s = h; + } + } + function Vi(t, e, n, a) { + t = null; + for (var s = e, u = !1; s !== null; ) { + if (!u) { + if ((s.flags & 524288) !== 0) u = !0; + else if ((s.flags & 262144) !== 0) break; + } + if (s.tag === 10) { + var h = s.alternate; + if (h === null) throw Error(r(387)); + if (((h = h.memoizedProps), h !== null)) { + var x = s.type; + be(s.pendingProps.value, h.value) || (t !== null ? t.push(x) : (t = [x])); + } + } else if (s === St.current) { + if (((h = s.alternate), h === null)) throw Error(r(387)); + h.memoizedState.memoizedState !== s.memoizedState.memoizedState && + (t !== null ? t.push(nl) : (t = [nl])); + } + s = s.return; + } + (t !== null && uo(e, t, n, a), (e.flags |= 262144)); + } + function Fl(t) { + for (t = t.firstContext; t !== null; ) { + if (!be(t.context._currentValue, t.memoizedValue)) return !0; + t = t.next; + } + return !1; + } + function si(t) { + ((li = t), (nn = null), (t = t.dependencies), t !== null && (t.firstContext = null)); + } + function te(t) { + return Td(li, t); + } + function Pl(t, e) { + return (li === null && si(t), Td(t, e)); + } + function Td(t, e) { + var n = e._currentValue; + if (((e = { context: e, memoizedValue: n, next: null }), nn === null)) { + if (t === null) throw Error(r(308)); + ((nn = e), (t.dependencies = { lanes: 0, firstContext: e }), (t.flags |= 524288)); + } else nn = nn.next = e; + return n; + } + var G1 = + typeof AbortController < 'u' + ? AbortController + : function () { + var t = [], + e = (this.signal = { + aborted: !1, + addEventListener: function (n, a) { + t.push(a); + }, + }); + this.abort = function () { + ((e.aborted = !0), + t.forEach(function (n) { + return n(); + })); + }; + }, + X1 = i.unstable_scheduleCallback, + Z1 = i.unstable_NormalPriority, + Yt = { + $$typeof: V, + Consumer: null, + Provider: null, + _currentValue: null, + _currentValue2: null, + _threadCount: 0, + }; + function co() { + return { controller: new G1(), data: new Map(), refCount: 0 }; + } + function La(t) { + (t.refCount--, + t.refCount === 0 && + X1(Z1, function () { + t.controller.abort(); + })); + } + var Na = null, + fo = 0, + Bi = 0, + Ui = null; + function Q1(t, e) { + if (Na === null) { + var n = (Na = []); + ((fo = 0), + (Bi = pu()), + (Ui = { + status: 'pending', + value: void 0, + then: function (a) { + n.push(a); + }, + })); + } + return (fo++, e.then(Ad, Ad), e); + } + function Ad() { + if (--fo === 0 && Na !== null) { + Ui !== null && (Ui.status = 'fulfilled'); + var t = Na; + ((Na = null), (Bi = 0), (Ui = null)); + for (var e = 0; e < t.length; e++) (0, t[e])(); + } + } + function K1(t, e) { + var n = [], + a = { + status: 'pending', + value: null, + reason: null, + then: function (s) { + n.push(s); + }, + }; + return ( + t.then( + function () { + ((a.status = 'fulfilled'), (a.value = e)); + for (var s = 0; s < n.length; s++) (0, n[s])(e); + }, + function (s) { + for (a.status = 'rejected', a.reason = s, s = 0; s < n.length; s++) (0, n[s])(void 0); + } + ), + a + ); + } + var Cd = z.S; + z.S = function (t, e) { + ((om = ge()), + typeof e == 'object' && e !== null && typeof e.then == 'function' && Q1(t, e), + Cd !== null && Cd(t, e)); + }; + var ri = A(null); + function ho() { + var t = ri.current; + return t !== null ? t : Et.pooledCache; + } + function $l(t, e) { + e === null ? X(ri, ri.current) : X(ri, e.pool); + } + function Md() { + var t = ho(); + return t === null ? null : { parent: Yt._currentValue, pool: t }; + } + var Hi = Error(r(460)), + mo = Error(r(474)), + Il = Error(r(542)), + ts = { then: function () {} }; + function Ed(t) { + return ((t = t.status), t === 'fulfilled' || t === 'rejected'); + } + function Dd(t, e, n) { + switch ( + ((n = t[n]), n === void 0 ? t.push(e) : n !== e && (e.then($e, $e), (e = n)), e.status) + ) { + case 'fulfilled': + return e.value; + case 'rejected': + throw ((t = e.reason), Ld(t), t); + default: + if (typeof e.status == 'string') e.then($e, $e); + else { + if (((t = Et), t !== null && 100 < t.shellSuspendCounter)) throw Error(r(482)); + ((t = e), + (t.status = 'pending'), + t.then( + function (a) { + if (e.status === 'pending') { + var s = e; + ((s.status = 'fulfilled'), (s.value = a)); + } + }, + function (a) { + if (e.status === 'pending') { + var s = e; + ((s.status = 'rejected'), (s.reason = a)); + } + } + )); + } + switch (e.status) { + case 'fulfilled': + return e.value; + case 'rejected': + throw ((t = e.reason), Ld(t), t); + } + throw ((ui = e), Hi); + } + } + function oi(t) { + try { + var e = t._init; + return e(t._payload); + } catch (n) { + throw n !== null && typeof n == 'object' && typeof n.then == 'function' ? ((ui = n), Hi) : n; + } + } + var ui = null; + function wd() { + if (ui === null) throw Error(r(459)); + var t = ui; + return ((ui = null), t); + } + function Ld(t) { + if (t === Hi || t === Il) throw Error(r(483)); + } + var Yi = null, + _a = 0; + function es(t) { + var e = _a; + return ((_a += 1), Yi === null && (Yi = []), Dd(Yi, t, e)); + } + function za(t, e) { + ((e = e.props.ref), (t.ref = e !== void 0 ? e : null)); + } + function ns(t, e) { + throw e.$$typeof === T + ? Error(r(525)) + : ((t = Object.prototype.toString.call(e)), + Error( + r( + 31, + t === '[object Object]' ? 'object with keys {' + Object.keys(e).join(', ') + '}' : t + ) + )); + } + function Nd(t) { + function e(C, j) { + if (t) { + var M = C.deletions; + M === null ? ((C.deletions = [j]), (C.flags |= 16)) : M.push(j); + } + } + function n(C, j) { + if (!t) return null; + for (; j !== null; ) (e(C, j), (j = j.sibling)); + return null; + } + function a(C) { + for (var j = new Map(); C !== null; ) + (C.key !== null ? j.set(C.key, C) : j.set(C.index, C), (C = C.sibling)); + return j; + } + function s(C, j) { + return ((C = tn(C, j)), (C.index = 0), (C.sibling = null), C); + } + function u(C, j, M) { + return ( + (C.index = M), + t + ? ((M = C.alternate), + M !== null + ? ((M = M.index), M < j ? ((C.flags |= 67108866), j) : M) + : ((C.flags |= 67108866), j)) + : ((C.flags |= 1048576), j) + ); + } + function h(C) { + return (t && C.alternate === null && (C.flags |= 67108866), C); + } + function x(C, j, M, R) { + return j === null || j.tag !== 6 + ? ((j = eo(M, C.mode, R)), (j.return = C), j) + : ((j = s(j, M)), (j.return = C), j); + } + function S(C, j, M, R) { + var tt = M.type; + return tt === H + ? _(C, j, M.props.children, R, M.key) + : j !== null && + (j.elementType === tt || + (typeof tt == 'object' && tt !== null && tt.$$typeof === K && oi(tt) === j.type)) + ? ((j = s(j, M.props)), za(j, M), (j.return = C), j) + : ((j = Wl(M.type, M.key, M.props, null, C.mode, R)), za(j, M), (j.return = C), j); + } + function E(C, j, M, R) { + return j === null || + j.tag !== 4 || + j.stateNode.containerInfo !== M.containerInfo || + j.stateNode.implementation !== M.implementation + ? ((j = no(M, C.mode, R)), (j.return = C), j) + : ((j = s(j, M.children || [])), (j.return = C), j); + } + function _(C, j, M, R, tt) { + return j === null || j.tag !== 7 + ? ((j = ii(M, C.mode, R, tt)), (j.return = C), j) + : ((j = s(j, M)), (j.return = C), j); + } + function O(C, j, M) { + if ((typeof j == 'string' && j !== '') || typeof j == 'number' || typeof j == 'bigint') + return ((j = eo('' + j, C.mode, M)), (j.return = C), j); + if (typeof j == 'object' && j !== null) { + switch (j.$$typeof) { + case w: + return ((M = Wl(j.type, j.key, j.props, null, C.mode, M)), za(M, j), (M.return = C), M); + case N: + return ((j = no(j, C.mode, M)), (j.return = C), j); + case K: + return ((j = oi(j)), O(C, j, M)); + } + if (Qt(j) || Nt(j)) return ((j = ii(j, C.mode, M, null)), (j.return = C), j); + if (typeof j.then == 'function') return O(C, es(j), M); + if (j.$$typeof === V) return O(C, Pl(C, j), M); + ns(C, j); + } + return null; + } + function D(C, j, M, R) { + var tt = j !== null ? j.key : null; + if ((typeof M == 'string' && M !== '') || typeof M == 'number' || typeof M == 'bigint') + return tt !== null ? null : x(C, j, '' + M, R); + if (typeof M == 'object' && M !== null) { + switch (M.$$typeof) { + case w: + return M.key === tt ? S(C, j, M, R) : null; + case N: + return M.key === tt ? E(C, j, M, R) : null; + case K: + return ((M = oi(M)), D(C, j, M, R)); + } + if (Qt(M) || Nt(M)) return tt !== null ? null : _(C, j, M, R, null); + if (typeof M.then == 'function') return D(C, j, es(M), R); + if (M.$$typeof === V) return D(C, j, Pl(C, M), R); + ns(C, M); + } + return null; + } + function L(C, j, M, R, tt) { + if ((typeof R == 'string' && R !== '') || typeof R == 'number' || typeof R == 'bigint') + return ((C = C.get(M) || null), x(j, C, '' + R, tt)); + if (typeof R == 'object' && R !== null) { + switch (R.$$typeof) { + case w: + return ((C = C.get(R.key === null ? M : R.key) || null), S(j, C, R, tt)); + case N: + return ((C = C.get(R.key === null ? M : R.key) || null), E(j, C, R, tt)); + case K: + return ((R = oi(R)), L(C, j, M, R, tt)); + } + if (Qt(R) || Nt(R)) return ((C = C.get(M) || null), _(j, C, R, tt, null)); + if (typeof R.then == 'function') return L(C, j, M, es(R), tt); + if (R.$$typeof === V) return L(C, j, M, Pl(j, R), tt); + ns(j, R); + } + return null; + } + function W(C, j, M, R) { + for ( + var tt = null, xt = null, I = j, rt = (j = 0), mt = null; + I !== null && rt < M.length; + rt++ + ) { + I.index > rt ? ((mt = I), (I = null)) : (mt = I.sibling); + var vt = D(C, I, M[rt], R); + if (vt === null) { + I === null && (I = mt); + break; + } + (t && I && vt.alternate === null && e(C, I), + (j = u(vt, j, rt)), + xt === null ? (tt = vt) : (xt.sibling = vt), + (xt = vt), + (I = mt)); + } + if (rt === M.length) return (n(C, I), pt && en(C, rt), tt); + if (I === null) { + for (; rt < M.length; rt++) + ((I = O(C, M[rt], R)), + I !== null && ((j = u(I, j, rt)), xt === null ? (tt = I) : (xt.sibling = I), (xt = I))); + return (pt && en(C, rt), tt); + } + for (I = a(I); rt < M.length; rt++) + ((mt = L(I, C, rt, M[rt], R)), + mt !== null && + (t && mt.alternate !== null && I.delete(mt.key === null ? rt : mt.key), + (j = u(mt, j, rt)), + xt === null ? (tt = mt) : (xt.sibling = mt), + (xt = mt))); + return ( + t && + I.forEach(function (qn) { + return e(C, qn); + }), + pt && en(C, rt), + tt + ); + } + function et(C, j, M, R) { + if (M == null) throw Error(r(151)); + for ( + var tt = null, xt = null, I = j, rt = (j = 0), mt = null, vt = M.next(); + I !== null && !vt.done; + rt++, vt = M.next() + ) { + I.index > rt ? ((mt = I), (I = null)) : (mt = I.sibling); + var qn = D(C, I, vt.value, R); + if (qn === null) { + I === null && (I = mt); + break; + } + (t && I && qn.alternate === null && e(C, I), + (j = u(qn, j, rt)), + xt === null ? (tt = qn) : (xt.sibling = qn), + (xt = qn), + (I = mt)); + } + if (vt.done) return (n(C, I), pt && en(C, rt), tt); + if (I === null) { + for (; !vt.done; rt++, vt = M.next()) + ((vt = O(C, vt.value, R)), + vt !== null && + ((j = u(vt, j, rt)), xt === null ? (tt = vt) : (xt.sibling = vt), (xt = vt))); + return (pt && en(C, rt), tt); + } + for (I = a(I); !vt.done; rt++, vt = M.next()) + ((vt = L(I, C, rt, vt.value, R)), + vt !== null && + (t && vt.alternate !== null && I.delete(vt.key === null ? rt : vt.key), + (j = u(vt, j, rt)), + xt === null ? (tt = vt) : (xt.sibling = vt), + (xt = vt))); + return ( + t && + I.forEach(function (av) { + return e(C, av); + }), + pt && en(C, rt), + tt + ); + } + function Mt(C, j, M, R) { + if ( + (typeof M == 'object' && + M !== null && + M.type === H && + M.key === null && + (M = M.props.children), + typeof M == 'object' && M !== null) + ) { + switch (M.$$typeof) { + case w: + t: { + for (var tt = M.key; j !== null; ) { + if (j.key === tt) { + if (((tt = M.type), tt === H)) { + if (j.tag === 7) { + (n(C, j.sibling), (R = s(j, M.props.children)), (R.return = C), (C = R)); + break t; + } + } else if ( + j.elementType === tt || + (typeof tt == 'object' && tt !== null && tt.$$typeof === K && oi(tt) === j.type) + ) { + (n(C, j.sibling), (R = s(j, M.props)), za(R, M), (R.return = C), (C = R)); + break t; + } + n(C, j); + break; + } else e(C, j); + j = j.sibling; + } + M.type === H + ? ((R = ii(M.props.children, C.mode, R, M.key)), (R.return = C), (C = R)) + : ((R = Wl(M.type, M.key, M.props, null, C.mode, R)), + za(R, M), + (R.return = C), + (C = R)); + } + return h(C); + case N: + t: { + for (tt = M.key; j !== null; ) { + if (j.key === tt) + if ( + j.tag === 4 && + j.stateNode.containerInfo === M.containerInfo && + j.stateNode.implementation === M.implementation + ) { + (n(C, j.sibling), (R = s(j, M.children || [])), (R.return = C), (C = R)); + break t; + } else { + n(C, j); + break; + } + else e(C, j); + j = j.sibling; + } + ((R = no(M, C.mode, R)), (R.return = C), (C = R)); + } + return h(C); + case K: + return ((M = oi(M)), Mt(C, j, M, R)); + } + if (Qt(M)) return W(C, j, M, R); + if (Nt(M)) { + if (((tt = Nt(M)), typeof tt != 'function')) throw Error(r(150)); + return ((M = tt.call(M)), et(C, j, M, R)); + } + if (typeof M.then == 'function') return Mt(C, j, es(M), R); + if (M.$$typeof === V) return Mt(C, j, Pl(C, M), R); + ns(C, M); + } + return (typeof M == 'string' && M !== '') || typeof M == 'number' || typeof M == 'bigint' + ? ((M = '' + M), + j !== null && j.tag === 6 + ? (n(C, j.sibling), (R = s(j, M)), (R.return = C), (C = R)) + : (n(C, j), (R = eo(M, C.mode, R)), (R.return = C), (C = R)), + h(C)) + : n(C, j); + } + return function (C, j, M, R) { + try { + _a = 0; + var tt = Mt(C, j, M, R); + return ((Yi = null), tt); + } catch (I) { + if (I === Hi || I === Il) throw I; + var xt = Se(29, I, null, C.mode); + return ((xt.lanes = R), (xt.return = C), xt); + } + }; + } + var ci = Nd(!0), + _d = Nd(!1), + Cn = !1; + function po(t) { + t.updateQueue = { + baseState: t.memoizedState, + firstBaseUpdate: null, + lastBaseUpdate: null, + shared: { pending: null, lanes: 0, hiddenCallbacks: null }, + callbacks: null, + }; + } + function yo(t, e) { + ((t = t.updateQueue), + e.updateQueue === t && + (e.updateQueue = { + baseState: t.baseState, + firstBaseUpdate: t.firstBaseUpdate, + lastBaseUpdate: t.lastBaseUpdate, + shared: t.shared, + callbacks: null, + })); + } + function Mn(t) { + return { lane: t, tag: 0, payload: null, callback: null, next: null }; + } + function En(t, e, n) { + var a = t.updateQueue; + if (a === null) return null; + if (((a = a.shared), (bt & 2) !== 0)) { + var s = a.pending; + return ( + s === null ? (e.next = e) : ((e.next = s.next), (s.next = e)), + (a.pending = e), + (e = Kl(t)), + pd(t, null, n), + e + ); + } + return (Ql(t, a, e, n), Kl(t)); + } + function Ra(t, e, n) { + if (((e = e.updateQueue), e !== null && ((e = e.shared), (n & 4194048) !== 0))) { + var a = e.lanes; + ((a &= t.pendingLanes), (n |= a), (e.lanes = n), Tf(t, n)); + } + } + function go(t, e) { + var n = t.updateQueue, + a = t.alternate; + if (a !== null && ((a = a.updateQueue), n === a)) { + var s = null, + u = null; + if (((n = n.firstBaseUpdate), n !== null)) { + do { + var h = { lane: n.lane, tag: n.tag, payload: n.payload, callback: null, next: null }; + (u === null ? (s = u = h) : (u = u.next = h), (n = n.next)); + } while (n !== null); + u === null ? (s = u = e) : (u = u.next = e); + } else s = u = e; + ((n = { + baseState: a.baseState, + firstBaseUpdate: s, + lastBaseUpdate: u, + shared: a.shared, + callbacks: a.callbacks, + }), + (t.updateQueue = n)); + return; + } + ((t = n.lastBaseUpdate), + t === null ? (n.firstBaseUpdate = e) : (t.next = e), + (n.lastBaseUpdate = e)); + } + var xo = !1; + function Oa() { + if (xo) { + var t = Ui; + if (t !== null) throw t; + } + } + function ka(t, e, n, a) { + xo = !1; + var s = t.updateQueue; + Cn = !1; + var u = s.firstBaseUpdate, + h = s.lastBaseUpdate, + x = s.shared.pending; + if (x !== null) { + s.shared.pending = null; + var S = x, + E = S.next; + ((S.next = null), h === null ? (u = E) : (h.next = E), (h = S)); + var _ = t.alternate; + _ !== null && + ((_ = _.updateQueue), + (x = _.lastBaseUpdate), + x !== h && (x === null ? (_.firstBaseUpdate = E) : (x.next = E), (_.lastBaseUpdate = S))); + } + if (u !== null) { + var O = s.baseState; + ((h = 0), (_ = E = S = null), (x = u)); + do { + var D = x.lane & -536870913, + L = D !== x.lane; + if (L ? (ht & D) === D : (a & D) === D) { + (D !== 0 && D === Bi && (xo = !0), + _ !== null && + (_ = _.next = + { lane: 0, tag: x.tag, payload: x.payload, callback: null, next: null })); + t: { + var W = t, + et = x; + D = e; + var Mt = n; + switch (et.tag) { + case 1: + if (((W = et.payload), typeof W == 'function')) { + O = W.call(Mt, O, D); + break t; + } + O = W; + break t; + case 3: + W.flags = (W.flags & -65537) | 128; + case 0: + if ( + ((W = et.payload), (D = typeof W == 'function' ? W.call(Mt, O, D) : W), D == null) + ) + break t; + O = b({}, O, D); + break t; + case 2: + Cn = !0; + } + } + ((D = x.callback), + D !== null && + ((t.flags |= 64), + L && (t.flags |= 8192), + (L = s.callbacks), + L === null ? (s.callbacks = [D]) : L.push(D))); + } else + ((L = { lane: D, tag: x.tag, payload: x.payload, callback: x.callback, next: null }), + _ === null ? ((E = _ = L), (S = O)) : (_ = _.next = L), + (h |= D)); + if (((x = x.next), x === null)) { + if (((x = s.shared.pending), x === null)) break; + ((L = x), + (x = L.next), + (L.next = null), + (s.lastBaseUpdate = L), + (s.shared.pending = null)); + } + } while (!0); + (_ === null && (S = O), + (s.baseState = S), + (s.firstBaseUpdate = E), + (s.lastBaseUpdate = _), + u === null && (s.shared.lanes = 0), + (_n |= h), + (t.lanes = h), + (t.memoizedState = O)); + } + } + function zd(t, e) { + if (typeof t != 'function') throw Error(r(191, t)); + t.call(e); + } + function Rd(t, e) { + var n = t.callbacks; + if (n !== null) for (t.callbacks = null, t = 0; t < n.length; t++) zd(n[t], e); + } + var qi = A(null), + is = A(0); + function Od(t, e) { + ((t = hn), X(is, t), X(qi, e), (hn = t | e.baseLanes)); + } + function vo() { + (X(is, hn), X(qi, qi.current)); + } + function bo() { + ((hn = is.current), k(qi), k(is)); + } + var je = A(null), + Re = null; + function Dn(t) { + var e = t.alternate; + (X(Bt, Bt.current & 1), + X(je, t), + Re === null && (e === null || qi.current !== null || e.memoizedState !== null) && (Re = t)); + } + function So(t) { + (X(Bt, Bt.current), X(je, t), Re === null && (Re = t)); + } + function kd(t) { + t.tag === 22 ? (X(Bt, Bt.current), X(je, t), Re === null && (Re = t)) : wn(); + } + function wn() { + (X(Bt, Bt.current), X(je, je.current)); + } + function Te(t) { + (k(je), Re === t && (Re = null), k(Bt)); + } + var Bt = A(0); + function as(t) { + for (var e = t; e !== null; ) { + if (e.tag === 13) { + var n = e.memoizedState; + if (n !== null && ((n = n.dehydrated), n === null || Eu(n) || Du(n))) return e; + } else if ( + e.tag === 19 && + (e.memoizedProps.revealOrder === 'forwards' || + e.memoizedProps.revealOrder === 'backwards' || + e.memoizedProps.revealOrder === 'unstable_legacy-backwards' || + e.memoizedProps.revealOrder === 'together') + ) { + if ((e.flags & 128) !== 0) return e; + } else if (e.child !== null) { + ((e.child.return = e), (e = e.child)); + continue; + } + if (e === t) break; + for (; e.sibling === null; ) { + if (e.return === null || e.return === t) return null; + e = e.return; + } + ((e.sibling.return = e.return), (e = e.sibling)); + } + return null; + } + var ln = 0, + st = null, + At = null, + qt = null, + ls = !1, + Gi = !1, + fi = !1, + ss = 0, + Va = 0, + Xi = null, + W1 = 0; + function Rt() { + throw Error(r(321)); + } + function jo(t, e) { + if (e === null) return !1; + for (var n = 0; n < e.length && n < t.length; n++) if (!be(t[n], e[n])) return !1; + return !0; + } + function To(t, e, n, a, s, u) { + return ( + (ln = u), + (st = e), + (e.memoizedState = null), + (e.updateQueue = null), + (e.lanes = 0), + (z.H = t === null || t.memoizedState === null ? vh : Bo), + (fi = !1), + (u = n(a, s)), + (fi = !1), + Gi && (u = Bd(e, n, a, s)), + Vd(t), + u + ); + } + function Vd(t) { + z.H = Ha; + var e = At !== null && At.next !== null; + if (((ln = 0), (qt = At = st = null), (ls = !1), (Va = 0), (Xi = null), e)) throw Error(r(300)); + t === null || Gt || ((t = t.dependencies), t !== null && Fl(t) && (Gt = !0)); + } + function Bd(t, e, n, a) { + st = t; + var s = 0; + do { + if ((Gi && (Xi = null), (Va = 0), (Gi = !1), 25 <= s)) throw Error(r(301)); + if (((s += 1), (qt = At = null), t.updateQueue != null)) { + var u = t.updateQueue; + ((u.lastEffect = null), + (u.events = null), + (u.stores = null), + u.memoCache != null && (u.memoCache.index = 0)); + } + ((z.H = bh), (u = e(n, a))); + } while (Gi); + return u; + } + function J1() { + var t = z.H, + e = t.useState()[0]; + return ( + (e = typeof e.then == 'function' ? Ba(e) : e), + (t = t.useState()[0]), + (At !== null ? At.memoizedState : null) !== t && (st.flags |= 1024), + e + ); + } + function Ao() { + var t = ss !== 0; + return ((ss = 0), t); + } + function Co(t, e, n) { + ((e.updateQueue = t.updateQueue), (e.flags &= -2053), (t.lanes &= ~n)); + } + function Mo(t) { + if (ls) { + for (t = t.memoizedState; t !== null; ) { + var e = t.queue; + (e !== null && (e.pending = null), (t = t.next)); + } + ls = !1; + } + ((ln = 0), (qt = At = st = null), (Gi = !1), (Va = ss = 0), (Xi = null)); + } + function re() { + var t = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; + return (qt === null ? (st.memoizedState = qt = t) : (qt = qt.next = t), qt); + } + function Ut() { + if (At === null) { + var t = st.alternate; + t = t !== null ? t.memoizedState : null; + } else t = At.next; + var e = qt === null ? st.memoizedState : qt.next; + if (e !== null) ((qt = e), (At = t)); + else { + if (t === null) throw st.alternate === null ? Error(r(467)) : Error(r(310)); + ((At = t), + (t = { + memoizedState: At.memoizedState, + baseState: At.baseState, + baseQueue: At.baseQueue, + queue: At.queue, + next: null, + }), + qt === null ? (st.memoizedState = qt = t) : (qt = qt.next = t)); + } + return qt; + } + function rs() { + return { lastEffect: null, events: null, stores: null, memoCache: null }; + } + function Ba(t) { + var e = Va; + return ( + (Va += 1), + Xi === null && (Xi = []), + (t = Dd(Xi, t, e)), + (e = st), + (qt === null ? e.memoizedState : qt.next) === null && + ((e = e.alternate), (z.H = e === null || e.memoizedState === null ? vh : Bo)), + t + ); + } + function os(t) { + if (t !== null && typeof t == 'object') { + if (typeof t.then == 'function') return Ba(t); + if (t.$$typeof === V) return te(t); + } + throw Error(r(438, String(t))); + } + function Eo(t) { + var e = null, + n = st.updateQueue; + if ((n !== null && (e = n.memoCache), e == null)) { + var a = st.alternate; + a !== null && + ((a = a.updateQueue), + a !== null && + ((a = a.memoCache), + a != null && + (e = { + data: a.data.map(function (s) { + return s.slice(); + }), + index: 0, + }))); + } + if ( + (e == null && (e = { data: [], index: 0 }), + n === null && ((n = rs()), (st.updateQueue = n)), + (n.memoCache = e), + (n = e.data[e.index]), + n === void 0) + ) + for (n = e.data[e.index] = Array(t), a = 0; a < t; a++) n[a] = yt; + return (e.index++, n); + } + function sn(t, e) { + return typeof e == 'function' ? e(t) : e; + } + function us(t) { + var e = Ut(); + return Do(e, At, t); + } + function Do(t, e, n) { + var a = t.queue; + if (a === null) throw Error(r(311)); + a.lastRenderedReducer = n; + var s = t.baseQueue, + u = a.pending; + if (u !== null) { + if (s !== null) { + var h = s.next; + ((s.next = u.next), (u.next = h)); + } + ((e.baseQueue = s = u), (a.pending = null)); + } + if (((u = t.baseState), s === null)) t.memoizedState = u; + else { + e = s.next; + var x = (h = null), + S = null, + E = e, + _ = !1; + do { + var O = E.lane & -536870913; + if (O !== E.lane ? (ht & O) === O : (ln & O) === O) { + var D = E.revertLane; + if (D === 0) + (S !== null && + (S = S.next = + { + lane: 0, + revertLane: 0, + gesture: null, + action: E.action, + hasEagerState: E.hasEagerState, + eagerState: E.eagerState, + next: null, + }), + O === Bi && (_ = !0)); + else if ((ln & D) === D) { + ((E = E.next), D === Bi && (_ = !0)); + continue; + } else + ((O = { + lane: 0, + revertLane: E.revertLane, + gesture: null, + action: E.action, + hasEagerState: E.hasEagerState, + eagerState: E.eagerState, + next: null, + }), + S === null ? ((x = S = O), (h = u)) : (S = S.next = O), + (st.lanes |= D), + (_n |= D)); + ((O = E.action), fi && n(u, O), (u = E.hasEagerState ? E.eagerState : n(u, O))); + } else + ((D = { + lane: O, + revertLane: E.revertLane, + gesture: E.gesture, + action: E.action, + hasEagerState: E.hasEagerState, + eagerState: E.eagerState, + next: null, + }), + S === null ? ((x = S = D), (h = u)) : (S = S.next = D), + (st.lanes |= O), + (_n |= O)); + E = E.next; + } while (E !== null && E !== e); + if ( + (S === null ? (h = u) : (S.next = x), + !be(u, t.memoizedState) && ((Gt = !0), _ && ((n = Ui), n !== null))) + ) + throw n; + ((t.memoizedState = u), (t.baseState = h), (t.baseQueue = S), (a.lastRenderedState = u)); + } + return (s === null && (a.lanes = 0), [t.memoizedState, a.dispatch]); + } + function wo(t) { + var e = Ut(), + n = e.queue; + if (n === null) throw Error(r(311)); + n.lastRenderedReducer = t; + var a = n.dispatch, + s = n.pending, + u = e.memoizedState; + if (s !== null) { + n.pending = null; + var h = (s = s.next); + do ((u = t(u, h.action)), (h = h.next)); + while (h !== s); + (be(u, e.memoizedState) || (Gt = !0), + (e.memoizedState = u), + e.baseQueue === null && (e.baseState = u), + (n.lastRenderedState = u)); + } + return [u, a]; + } + function Ud(t, e, n) { + var a = st, + s = Ut(), + u = pt; + if (u) { + if (n === void 0) throw Error(r(407)); + n = n(); + } else n = e(); + var h = !be((At || s).memoizedState, n); + if ( + (h && ((s.memoizedState = n), (Gt = !0)), + (s = s.queue), + _o(qd.bind(null, a, s, t), [t]), + s.getSnapshot !== e || h || (qt !== null && qt.memoizedState.tag & 1)) + ) { + if ( + ((a.flags |= 2048), + Zi(9, { destroy: void 0 }, Yd.bind(null, a, s, n, e), null), + Et === null) + ) + throw Error(r(349)); + u || (ln & 127) !== 0 || Hd(a, e, n); + } + return n; + } + function Hd(t, e, n) { + ((t.flags |= 16384), + (t = { getSnapshot: e, value: n }), + (e = st.updateQueue), + e === null + ? ((e = rs()), (st.updateQueue = e), (e.stores = [t])) + : ((n = e.stores), n === null ? (e.stores = [t]) : n.push(t))); + } + function Yd(t, e, n, a) { + ((e.value = n), (e.getSnapshot = a), Gd(e) && Xd(t)); + } + function qd(t, e, n) { + return n(function () { + Gd(e) && Xd(t); + }); + } + function Gd(t) { + var e = t.getSnapshot; + t = t.value; + try { + var n = e(); + return !be(t, n); + } catch { + return !0; + } + } + function Xd(t) { + var e = ni(t, 2); + e !== null && ye(e, t, 2); + } + function Lo(t) { + var e = re(); + if (typeof t == 'function') { + var n = t; + if (((t = n()), fi)) { + xn(!0); + try { + n(); + } finally { + xn(!1); + } + } + } + return ( + (e.memoizedState = e.baseState = t), + (e.queue = { + pending: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: sn, + lastRenderedState: t, + }), + e + ); + } + function Zd(t, e, n, a) { + return ((t.baseState = n), Do(t, At, typeof a == 'function' ? a : sn)); + } + function F1(t, e, n, a, s) { + if (ds(t)) throw Error(r(485)); + if (((t = e.action), t !== null)) { + var u = { + payload: s, + action: t, + next: null, + isTransition: !0, + status: 'pending', + value: null, + reason: null, + listeners: [], + then: function (h) { + u.listeners.push(h); + }, + }; + (z.T !== null ? n(!0) : (u.isTransition = !1), + a(u), + (n = e.pending), + n === null + ? ((u.next = e.pending = u), Qd(e, u)) + : ((u.next = n.next), (e.pending = n.next = u))); + } + } + function Qd(t, e) { + var n = e.action, + a = e.payload, + s = t.state; + if (e.isTransition) { + var u = z.T, + h = {}; + z.T = h; + try { + var x = n(s, a), + S = z.S; + (S !== null && S(h, x), Kd(t, e, x)); + } catch (E) { + No(t, e, E); + } finally { + (u !== null && h.types !== null && (u.types = h.types), (z.T = u)); + } + } else + try { + ((u = n(s, a)), Kd(t, e, u)); + } catch (E) { + No(t, e, E); + } + } + function Kd(t, e, n) { + n !== null && typeof n == 'object' && typeof n.then == 'function' + ? n.then( + function (a) { + Wd(t, e, a); + }, + function (a) { + return No(t, e, a); + } + ) + : Wd(t, e, n); + } + function Wd(t, e, n) { + ((e.status = 'fulfilled'), + (e.value = n), + Jd(e), + (t.state = n), + (e = t.pending), + e !== null && + ((n = e.next), n === e ? (t.pending = null) : ((n = n.next), (e.next = n), Qd(t, n)))); + } + function No(t, e, n) { + var a = t.pending; + if (((t.pending = null), a !== null)) { + a = a.next; + do ((e.status = 'rejected'), (e.reason = n), Jd(e), (e = e.next)); + while (e !== a); + } + t.action = null; + } + function Jd(t) { + t = t.listeners; + for (var e = 0; e < t.length; e++) (0, t[e])(); + } + function Fd(t, e) { + return e; + } + function Pd(t, e) { + if (pt) { + var n = Et.formState; + if (n !== null) { + t: { + var a = st; + if (pt) { + if (wt) { + e: { + for (var s = wt, u = ze; s.nodeType !== 8; ) { + if (!u) { + s = null; + break e; + } + if (((s = Oe(s.nextSibling)), s === null)) { + s = null; + break e; + } + } + ((u = s.data), (s = u === 'F!' || u === 'F' ? s : null)); + } + if (s) { + ((wt = Oe(s.nextSibling)), (a = s.data === 'F!')); + break t; + } + } + Tn(a); + } + a = !1; + } + a && (e = n[0]); + } + } + return ( + (n = re()), + (n.memoizedState = n.baseState = e), + (a = { + pending: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: Fd, + lastRenderedState: e, + }), + (n.queue = a), + (n = yh.bind(null, st, a)), + (a.dispatch = n), + (a = Lo(!1)), + (u = Vo.bind(null, st, !1, a.queue)), + (a = re()), + (s = { state: e, dispatch: null, action: t, pending: null }), + (a.queue = s), + (n = F1.bind(null, st, s, u, n)), + (s.dispatch = n), + (a.memoizedState = t), + [e, n, !1] + ); + } + function $d(t) { + var e = Ut(); + return Id(e, At, t); + } + function Id(t, e, n) { + if ( + ((e = Do(t, e, Fd)[0]), + (t = us(sn)[0]), + typeof e == 'object' && e !== null && typeof e.then == 'function') + ) + try { + var a = Ba(e); + } catch (h) { + throw h === Hi ? Il : h; + } + else a = e; + e = Ut(); + var s = e.queue, + u = s.dispatch; + return ( + n !== e.memoizedState && + ((st.flags |= 2048), Zi(9, { destroy: void 0 }, P1.bind(null, s, n), null)), + [a, u, t] + ); + } + function P1(t, e) { + t.action = e; + } + function th(t) { + var e = Ut(), + n = At; + if (n !== null) return Id(e, n, t); + (Ut(), (e = e.memoizedState), (n = Ut())); + var a = n.queue.dispatch; + return ((n.memoizedState = t), [e, a, !1]); + } + function Zi(t, e, n, a) { + return ( + (t = { tag: t, create: n, deps: a, inst: e, next: null }), + (e = st.updateQueue), + e === null && ((e = rs()), (st.updateQueue = e)), + (n = e.lastEffect), + n === null + ? (e.lastEffect = t.next = t) + : ((a = n.next), (n.next = t), (t.next = a), (e.lastEffect = t)), + t + ); + } + function eh() { + return Ut().memoizedState; + } + function cs(t, e, n, a) { + var s = re(); + ((st.flags |= t), + (s.memoizedState = Zi(1 | e, { destroy: void 0 }, n, a === void 0 ? null : a))); + } + function fs(t, e, n, a) { + var s = Ut(); + a = a === void 0 ? null : a; + var u = s.memoizedState.inst; + At !== null && a !== null && jo(a, At.memoizedState.deps) + ? (s.memoizedState = Zi(e, u, n, a)) + : ((st.flags |= t), (s.memoizedState = Zi(1 | e, u, n, a))); + } + function nh(t, e) { + cs(8390656, 8, t, e); + } + function _o(t, e) { + fs(2048, 8, t, e); + } + function $1(t) { + st.flags |= 4; + var e = st.updateQueue; + if (e === null) ((e = rs()), (st.updateQueue = e), (e.events = [t])); + else { + var n = e.events; + n === null ? (e.events = [t]) : n.push(t); + } + } + function ih(t) { + var e = Ut().memoizedState; + return ( + $1({ ref: e, nextImpl: t }), + function () { + if ((bt & 2) !== 0) throw Error(r(440)); + return e.impl.apply(void 0, arguments); + } + ); + } + function ah(t, e) { + return fs(4, 2, t, e); + } + function lh(t, e) { + return fs(4, 4, t, e); + } + function sh(t, e) { + if (typeof e == 'function') { + t = t(); + var n = e(t); + return function () { + typeof n == 'function' ? n() : e(null); + }; + } + if (e != null) + return ( + (t = t()), + (e.current = t), + function () { + e.current = null; + } + ); + } + function rh(t, e, n) { + ((n = n != null ? n.concat([t]) : null), fs(4, 4, sh.bind(null, e, t), n)); + } + function zo() {} + function oh(t, e) { + var n = Ut(); + e = e === void 0 ? null : e; + var a = n.memoizedState; + return e !== null && jo(e, a[1]) ? a[0] : ((n.memoizedState = [t, e]), t); + } + function uh(t, e) { + var n = Ut(); + e = e === void 0 ? null : e; + var a = n.memoizedState; + if (e !== null && jo(e, a[1])) return a[0]; + if (((a = t()), fi)) { + xn(!0); + try { + t(); + } finally { + xn(!1); + } + } + return ((n.memoizedState = [a, e]), a); + } + function Ro(t, e, n) { + return n === void 0 || ((ln & 1073741824) !== 0 && (ht & 261930) === 0) + ? (t.memoizedState = e) + : ((t.memoizedState = n), (t = cm()), (st.lanes |= t), (_n |= t), n); + } + function ch(t, e, n, a) { + return be(n, e) + ? n + : qi.current !== null + ? ((t = Ro(t, n, a)), be(t, e) || (Gt = !0), t) + : (ln & 42) === 0 || ((ln & 1073741824) !== 0 && (ht & 261930) === 0) + ? ((Gt = !0), (t.memoizedState = n)) + : ((t = cm()), (st.lanes |= t), (_n |= t), e); + } + function fh(t, e, n, a, s) { + var u = B.p; + B.p = u !== 0 && 8 > u ? u : 8; + var h = z.T, + x = {}; + ((z.T = x), Vo(t, !1, e, n)); + try { + var S = s(), + E = z.S; + if ( + (E !== null && E(x, S), S !== null && typeof S == 'object' && typeof S.then == 'function') + ) { + var _ = K1(S, a); + Ua(t, e, _, Me(t)); + } else Ua(t, e, a, Me(t)); + } catch (O) { + Ua(t, e, { then: function () {}, status: 'rejected', reason: O }, Me()); + } finally { + ((B.p = u), h !== null && x.types !== null && (h.types = x.types), (z.T = h)); + } + } + function I1() {} + function Oo(t, e, n, a) { + if (t.tag !== 5) throw Error(r(476)); + var s = dh(t).queue; + fh( + t, + s, + e, + P, + n === null + ? I1 + : function () { + return (hh(t), n(a)); + } + ); + } + function dh(t) { + var e = t.memoizedState; + if (e !== null) return e; + e = { + memoizedState: P, + baseState: P, + baseQueue: null, + queue: { + pending: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: sn, + lastRenderedState: P, + }, + next: null, + }; + var n = {}; + return ( + (e.next = { + memoizedState: n, + baseState: n, + baseQueue: null, + queue: { + pending: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: sn, + lastRenderedState: n, + }, + next: null, + }), + (t.memoizedState = e), + (t = t.alternate), + t !== null && (t.memoizedState = e), + e + ); + } + function hh(t) { + var e = dh(t); + (e.next === null && (e = t.alternate.memoizedState), Ua(t, e.next.queue, {}, Me())); + } + function ko() { + return te(nl); + } + function mh() { + return Ut().memoizedState; + } + function ph() { + return Ut().memoizedState; + } + function tx(t) { + for (var e = t.return; e !== null; ) { + switch (e.tag) { + case 24: + case 3: + var n = Me(); + t = Mn(n); + var a = En(e, t, n); + (a !== null && (ye(a, e, n), Ra(a, e, n)), (e = { cache: co() }), (t.payload = e)); + return; + } + e = e.return; + } + } + function ex(t, e, n) { + var a = Me(); + ((n = { + lane: a, + revertLane: 0, + gesture: null, + action: n, + hasEagerState: !1, + eagerState: null, + next: null, + }), + ds(t) ? gh(e, n) : ((n = Ir(t, e, n, a)), n !== null && (ye(n, t, a), xh(n, e, a)))); + } + function yh(t, e, n) { + var a = Me(); + Ua(t, e, n, a); + } + function Ua(t, e, n, a) { + var s = { + lane: a, + revertLane: 0, + gesture: null, + action: n, + hasEagerState: !1, + eagerState: null, + next: null, + }; + if (ds(t)) gh(e, s); + else { + var u = t.alternate; + if ( + t.lanes === 0 && + (u === null || u.lanes === 0) && + ((u = e.lastRenderedReducer), u !== null) + ) + try { + var h = e.lastRenderedState, + x = u(h, n); + if (((s.hasEagerState = !0), (s.eagerState = x), be(x, h))) + return (Ql(t, e, s, 0), Et === null && Zl(), !1); + } catch {} + if (((n = Ir(t, e, s, a)), n !== null)) return (ye(n, t, a), xh(n, e, a), !0); + } + return !1; + } + function Vo(t, e, n, a) { + if ( + ((a = { + lane: 2, + revertLane: pu(), + gesture: null, + action: a, + hasEagerState: !1, + eagerState: null, + next: null, + }), + ds(t)) + ) { + if (e) throw Error(r(479)); + } else ((e = Ir(t, n, a, 2)), e !== null && ye(e, t, 2)); + } + function ds(t) { + var e = t.alternate; + return t === st || (e !== null && e === st); + } + function gh(t, e) { + Gi = ls = !0; + var n = t.pending; + (n === null ? (e.next = e) : ((e.next = n.next), (n.next = e)), (t.pending = e)); + } + function xh(t, e, n) { + if ((n & 4194048) !== 0) { + var a = e.lanes; + ((a &= t.pendingLanes), (n |= a), (e.lanes = n), Tf(t, n)); + } + } + var Ha = { + readContext: te, + use: os, + useCallback: Rt, + useContext: Rt, + useEffect: Rt, + useImperativeHandle: Rt, + useLayoutEffect: Rt, + useInsertionEffect: Rt, + useMemo: Rt, + useReducer: Rt, + useRef: Rt, + useState: Rt, + useDebugValue: Rt, + useDeferredValue: Rt, + useTransition: Rt, + useSyncExternalStore: Rt, + useId: Rt, + useHostTransitionStatus: Rt, + useFormState: Rt, + useActionState: Rt, + useOptimistic: Rt, + useMemoCache: Rt, + useCacheRefresh: Rt, + }; + Ha.useEffectEvent = Rt; + var vh = { + readContext: te, + use: os, + useCallback: function (t, e) { + return ((re().memoizedState = [t, e === void 0 ? null : e]), t); + }, + useContext: te, + useEffect: nh, + useImperativeHandle: function (t, e, n) { + ((n = n != null ? n.concat([t]) : null), cs(4194308, 4, sh.bind(null, e, t), n)); + }, + useLayoutEffect: function (t, e) { + return cs(4194308, 4, t, e); + }, + useInsertionEffect: function (t, e) { + cs(4, 2, t, e); + }, + useMemo: function (t, e) { + var n = re(); + e = e === void 0 ? null : e; + var a = t(); + if (fi) { + xn(!0); + try { + t(); + } finally { + xn(!1); + } + } + return ((n.memoizedState = [a, e]), a); + }, + useReducer: function (t, e, n) { + var a = re(); + if (n !== void 0) { + var s = n(e); + if (fi) { + xn(!0); + try { + n(e); + } finally { + xn(!1); + } + } + } else s = e; + return ( + (a.memoizedState = a.baseState = s), + (t = { + pending: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: t, + lastRenderedState: s, + }), + (a.queue = t), + (t = t.dispatch = ex.bind(null, st, t)), + [a.memoizedState, t] + ); + }, + useRef: function (t) { + var e = re(); + return ((t = { current: t }), (e.memoizedState = t)); + }, + useState: function (t) { + t = Lo(t); + var e = t.queue, + n = yh.bind(null, st, e); + return ((e.dispatch = n), [t.memoizedState, n]); + }, + useDebugValue: zo, + useDeferredValue: function (t, e) { + var n = re(); + return Ro(n, t, e); + }, + useTransition: function () { + var t = Lo(!1); + return ((t = fh.bind(null, st, t.queue, !0, !1)), (re().memoizedState = t), [!1, t]); + }, + useSyncExternalStore: function (t, e, n) { + var a = st, + s = re(); + if (pt) { + if (n === void 0) throw Error(r(407)); + n = n(); + } else { + if (((n = e()), Et === null)) throw Error(r(349)); + (ht & 127) !== 0 || Hd(a, e, n); + } + s.memoizedState = n; + var u = { value: n, getSnapshot: e }; + return ( + (s.queue = u), + nh(qd.bind(null, a, u, t), [t]), + (a.flags |= 2048), + Zi(9, { destroy: void 0 }, Yd.bind(null, a, u, n, e), null), + n + ); + }, + useId: function () { + var t = re(), + e = Et.identifierPrefix; + if (pt) { + var n = Qe, + a = Ze; + ((n = (a & ~(1 << (32 - ve(a) - 1))).toString(32) + n), + (e = '_' + e + 'R_' + n), + (n = ss++), + 0 < n && (e += 'H' + n.toString(32)), + (e += '_')); + } else ((n = W1++), (e = '_' + e + 'r_' + n.toString(32) + '_')); + return (t.memoizedState = e); + }, + useHostTransitionStatus: ko, + useFormState: Pd, + useActionState: Pd, + useOptimistic: function (t) { + var e = re(); + e.memoizedState = e.baseState = t; + var n = { + pending: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: null, + lastRenderedState: null, + }; + return ((e.queue = n), (e = Vo.bind(null, st, !0, n)), (n.dispatch = e), [t, e]); + }, + useMemoCache: Eo, + useCacheRefresh: function () { + return (re().memoizedState = tx.bind(null, st)); + }, + useEffectEvent: function (t) { + var e = re(), + n = { impl: t }; + return ( + (e.memoizedState = n), + function () { + if ((bt & 2) !== 0) throw Error(r(440)); + return n.impl.apply(void 0, arguments); + } + ); + }, + }, + Bo = { + readContext: te, + use: os, + useCallback: oh, + useContext: te, + useEffect: _o, + useImperativeHandle: rh, + useInsertionEffect: ah, + useLayoutEffect: lh, + useMemo: uh, + useReducer: us, + useRef: eh, + useState: function () { + return us(sn); + }, + useDebugValue: zo, + useDeferredValue: function (t, e) { + var n = Ut(); + return ch(n, At.memoizedState, t, e); + }, + useTransition: function () { + var t = us(sn)[0], + e = Ut().memoizedState; + return [typeof t == 'boolean' ? t : Ba(t), e]; + }, + useSyncExternalStore: Ud, + useId: mh, + useHostTransitionStatus: ko, + useFormState: $d, + useActionState: $d, + useOptimistic: function (t, e) { + var n = Ut(); + return Zd(n, At, t, e); + }, + useMemoCache: Eo, + useCacheRefresh: ph, + }; + Bo.useEffectEvent = ih; + var bh = { + readContext: te, + use: os, + useCallback: oh, + useContext: te, + useEffect: _o, + useImperativeHandle: rh, + useInsertionEffect: ah, + useLayoutEffect: lh, + useMemo: uh, + useReducer: wo, + useRef: eh, + useState: function () { + return wo(sn); + }, + useDebugValue: zo, + useDeferredValue: function (t, e) { + var n = Ut(); + return At === null ? Ro(n, t, e) : ch(n, At.memoizedState, t, e); + }, + useTransition: function () { + var t = wo(sn)[0], + e = Ut().memoizedState; + return [typeof t == 'boolean' ? t : Ba(t), e]; + }, + useSyncExternalStore: Ud, + useId: mh, + useHostTransitionStatus: ko, + useFormState: th, + useActionState: th, + useOptimistic: function (t, e) { + var n = Ut(); + return At !== null ? Zd(n, At, t, e) : ((n.baseState = t), [t, n.queue.dispatch]); + }, + useMemoCache: Eo, + useCacheRefresh: ph, + }; + bh.useEffectEvent = ih; + function Uo(t, e, n, a) { + ((e = t.memoizedState), + (n = n(a, e)), + (n = n == null ? e : b({}, e, n)), + (t.memoizedState = n), + t.lanes === 0 && (t.updateQueue.baseState = n)); + } + var Ho = { + enqueueSetState: function (t, e, n) { + t = t._reactInternals; + var a = Me(), + s = Mn(a); + ((s.payload = e), + n != null && (s.callback = n), + (e = En(t, s, a)), + e !== null && (ye(e, t, a), Ra(e, t, a))); + }, + enqueueReplaceState: function (t, e, n) { + t = t._reactInternals; + var a = Me(), + s = Mn(a); + ((s.tag = 1), + (s.payload = e), + n != null && (s.callback = n), + (e = En(t, s, a)), + e !== null && (ye(e, t, a), Ra(e, t, a))); + }, + enqueueForceUpdate: function (t, e) { + t = t._reactInternals; + var n = Me(), + a = Mn(n); + ((a.tag = 2), + e != null && (a.callback = e), + (e = En(t, a, n)), + e !== null && (ye(e, t, n), Ra(e, t, n))); + }, + }; + function Sh(t, e, n, a, s, u, h) { + return ( + (t = t.stateNode), + typeof t.shouldComponentUpdate == 'function' + ? t.shouldComponentUpdate(a, u, h) + : e.prototype && e.prototype.isPureReactComponent + ? !Ma(n, a) || !Ma(s, u) + : !0 + ); + } + function jh(t, e, n, a) { + ((t = e.state), + typeof e.componentWillReceiveProps == 'function' && e.componentWillReceiveProps(n, a), + typeof e.UNSAFE_componentWillReceiveProps == 'function' && + e.UNSAFE_componentWillReceiveProps(n, a), + e.state !== t && Ho.enqueueReplaceState(e, e.state, null)); + } + function di(t, e) { + var n = e; + if ('ref' in e) { + n = {}; + for (var a in e) a !== 'ref' && (n[a] = e[a]); + } + if ((t = t.defaultProps)) { + n === e && (n = b({}, n)); + for (var s in t) n[s] === void 0 && (n[s] = t[s]); + } + return n; + } + function Th(t) { + Xl(t); + } + function Ah(t) { + console.error(t); + } + function Ch(t) { + Xl(t); + } + function hs(t, e) { + try { + var n = t.onUncaughtError; + n(e.value, { componentStack: e.stack }); + } catch (a) { + setTimeout(function () { + throw a; + }); + } + } + function Mh(t, e, n) { + try { + var a = t.onCaughtError; + a(n.value, { componentStack: n.stack, errorBoundary: e.tag === 1 ? e.stateNode : null }); + } catch (s) { + setTimeout(function () { + throw s; + }); + } + } + function Yo(t, e, n) { + return ( + (n = Mn(n)), + (n.tag = 3), + (n.payload = { element: null }), + (n.callback = function () { + hs(t, e); + }), + n + ); + } + function Eh(t) { + return ((t = Mn(t)), (t.tag = 3), t); + } + function Dh(t, e, n, a) { + var s = n.type.getDerivedStateFromError; + if (typeof s == 'function') { + var u = a.value; + ((t.payload = function () { + return s(u); + }), + (t.callback = function () { + Mh(e, n, a); + })); + } + var h = n.stateNode; + h !== null && + typeof h.componentDidCatch == 'function' && + (t.callback = function () { + (Mh(e, n, a), + typeof s != 'function' && (zn === null ? (zn = new Set([this])) : zn.add(this))); + var x = a.stack; + this.componentDidCatch(a.value, { componentStack: x !== null ? x : '' }); + }); + } + function nx(t, e, n, a, s) { + if (((n.flags |= 32768), a !== null && typeof a == 'object' && typeof a.then == 'function')) { + if (((e = n.alternate), e !== null && Vi(e, n, s, !0), (n = je.current), n !== null)) { + switch (n.tag) { + case 31: + case 13: + return ( + Re === null ? Cs() : n.alternate === null && Ot === 0 && (Ot = 3), + (n.flags &= -257), + (n.flags |= 65536), + (n.lanes = s), + a === ts + ? (n.flags |= 16384) + : ((e = n.updateQueue), + e === null ? (n.updateQueue = new Set([a])) : e.add(a), + du(t, a, s)), + !1 + ); + case 22: + return ( + (n.flags |= 65536), + a === ts + ? (n.flags |= 16384) + : ((e = n.updateQueue), + e === null + ? ((e = { transitions: null, markerInstances: null, retryQueue: new Set([a]) }), + (n.updateQueue = e)) + : ((n = e.retryQueue), n === null ? (e.retryQueue = new Set([a])) : n.add(a)), + du(t, a, s)), + !1 + ); + } + throw Error(r(435, n.tag)); + } + return (du(t, a, s), Cs(), !1); + } + if (pt) + return ( + (e = je.current), + e !== null + ? ((e.flags & 65536) === 0 && (e.flags |= 256), + (e.flags |= 65536), + (e.lanes = s), + a !== lo && ((t = Error(r(422), { cause: a })), wa(Le(t, n)))) + : (a !== lo && ((e = Error(r(423), { cause: a })), wa(Le(e, n))), + (t = t.current.alternate), + (t.flags |= 65536), + (s &= -s), + (t.lanes |= s), + (a = Le(a, n)), + (s = Yo(t.stateNode, a, s)), + go(t, s), + Ot !== 4 && (Ot = 2)), + !1 + ); + var u = Error(r(520), { cause: a }); + if (((u = Le(u, n)), Wa === null ? (Wa = [u]) : Wa.push(u), Ot !== 4 && (Ot = 2), e === null)) + return !0; + ((a = Le(a, n)), (n = e)); + do { + switch (n.tag) { + case 3: + return ( + (n.flags |= 65536), + (t = s & -s), + (n.lanes |= t), + (t = Yo(n.stateNode, a, t)), + go(n, t), + !1 + ); + case 1: + if ( + ((e = n.type), + (u = n.stateNode), + (n.flags & 128) === 0 && + (typeof e.getDerivedStateFromError == 'function' || + (u !== null && + typeof u.componentDidCatch == 'function' && + (zn === null || !zn.has(u))))) + ) + return ( + (n.flags |= 65536), + (s &= -s), + (n.lanes |= s), + (s = Eh(s)), + Dh(s, t, n, a), + go(n, s), + !1 + ); + } + n = n.return; + } while (n !== null); + return !1; + } + var qo = Error(r(461)), + Gt = !1; + function ee(t, e, n, a) { + e.child = t === null ? _d(e, null, n, a) : ci(e, t.child, n, a); + } + function wh(t, e, n, a, s) { + n = n.render; + var u = e.ref; + if ('ref' in a) { + var h = {}; + for (var x in a) x !== 'ref' && (h[x] = a[x]); + } else h = a; + return ( + si(e), + (a = To(t, e, n, h, u, s)), + (x = Ao()), + t !== null && !Gt + ? (Co(t, e, s), rn(t, e, s)) + : (pt && x && io(e), (e.flags |= 1), ee(t, e, a, s), e.child) + ); + } + function Lh(t, e, n, a, s) { + if (t === null) { + var u = n.type; + return typeof u == 'function' && !to(u) && u.defaultProps === void 0 && n.compare === null + ? ((e.tag = 15), (e.type = u), Nh(t, e, u, a, s)) + : ((t = Wl(n.type, null, a, e, e.mode, s)), (t.ref = e.ref), (t.return = e), (e.child = t)); + } + if (((u = t.child), !Fo(t, s))) { + var h = u.memoizedProps; + if (((n = n.compare), (n = n !== null ? n : Ma), n(h, a) && t.ref === e.ref)) + return rn(t, e, s); + } + return ((e.flags |= 1), (t = tn(u, a)), (t.ref = e.ref), (t.return = e), (e.child = t)); + } + function Nh(t, e, n, a, s) { + if (t !== null) { + var u = t.memoizedProps; + if (Ma(u, a) && t.ref === e.ref) + if (((Gt = !1), (e.pendingProps = a = u), Fo(t, s))) (t.flags & 131072) !== 0 && (Gt = !0); + else return ((e.lanes = t.lanes), rn(t, e, s)); + } + return Go(t, e, n, a, s); + } + function _h(t, e, n, a) { + var s = a.children, + u = t !== null ? t.memoizedState : null; + if ( + (t === null && + e.stateNode === null && + (e.stateNode = { + _visibility: 1, + _pendingMarkers: null, + _retryCache: null, + _transitions: null, + }), + a.mode === 'hidden') + ) { + if ((e.flags & 128) !== 0) { + if (((u = u !== null ? u.baseLanes | n : n), t !== null)) { + for (a = e.child = t.child, s = 0; a !== null; ) + ((s = s | a.lanes | a.childLanes), (a = a.sibling)); + a = s & ~u; + } else ((a = 0), (e.child = null)); + return zh(t, e, u, n, a); + } + if ((n & 536870912) !== 0) + ((e.memoizedState = { baseLanes: 0, cachePool: null }), + t !== null && $l(e, u !== null ? u.cachePool : null), + u !== null ? Od(e, u) : vo(), + kd(e)); + else return ((a = e.lanes = 536870912), zh(t, e, u !== null ? u.baseLanes | n : n, n, a)); + } else + u !== null + ? ($l(e, u.cachePool), Od(e, u), wn(), (e.memoizedState = null)) + : (t !== null && $l(e, null), vo(), wn()); + return (ee(t, e, s, n), e.child); + } + function Ya(t, e) { + return ( + (t !== null && t.tag === 22) || + e.stateNode !== null || + (e.stateNode = { + _visibility: 1, + _pendingMarkers: null, + _retryCache: null, + _transitions: null, + }), + e.sibling + ); + } + function zh(t, e, n, a, s) { + var u = ho(); + return ( + (u = u === null ? null : { parent: Yt._currentValue, pool: u }), + (e.memoizedState = { baseLanes: n, cachePool: u }), + t !== null && $l(e, null), + vo(), + kd(e), + t !== null && Vi(t, e, a, !0), + (e.childLanes = s), + null + ); + } + function ms(t, e) { + return ( + (e = ys({ mode: e.mode, children: e.children }, t.mode)), + (e.ref = t.ref), + (t.child = e), + (e.return = t), + e + ); + } + function Rh(t, e, n) { + return ( + ci(e, t.child, null, n), + (t = ms(e, e.pendingProps)), + (t.flags |= 2), + Te(e), + (e.memoizedState = null), + t + ); + } + function ix(t, e, n) { + var a = e.pendingProps, + s = (e.flags & 128) !== 0; + if (((e.flags &= -129), t === null)) { + if (pt) { + if (a.mode === 'hidden') return ((t = ms(e, a)), (e.lanes = 536870912), Ya(null, t)); + if ( + (So(e), + (t = wt) + ? ((t = Qm(t, ze)), + (t = t !== null && t.data === '&' ? t : null), + t !== null && + ((e.memoizedState = { + dehydrated: t, + treeContext: Sn !== null ? { id: Ze, overflow: Qe } : null, + retryLane: 536870912, + hydrationErrors: null, + }), + (n = gd(t)), + (n.return = e), + (e.child = n), + (It = e), + (wt = null))) + : (t = null), + t === null) + ) + throw Tn(e); + return ((e.lanes = 536870912), null); + } + return ms(e, a); + } + var u = t.memoizedState; + if (u !== null) { + var h = u.dehydrated; + if ((So(e), s)) + if (e.flags & 256) ((e.flags &= -257), (e = Rh(t, e, n))); + else if (e.memoizedState !== null) ((e.child = t.child), (e.flags |= 128), (e = null)); + else throw Error(r(558)); + else if ((Gt || Vi(t, e, n, !1), (s = (n & t.childLanes) !== 0), Gt || s)) { + if (((a = Et), a !== null && ((h = Af(a, n)), h !== 0 && h !== u.retryLane))) + throw ((u.retryLane = h), ni(t, h), ye(a, t, h), qo); + (Cs(), (e = Rh(t, e, n))); + } else + ((t = u.treeContext), + (wt = Oe(h.nextSibling)), + (It = e), + (pt = !0), + (jn = null), + (ze = !1), + t !== null && bd(e, t), + (e = ms(e, a)), + (e.flags |= 4096)); + return e; + } + return ( + (t = tn(t.child, { mode: a.mode, children: a.children })), + (t.ref = e.ref), + (e.child = t), + (t.return = e), + t + ); + } + function ps(t, e) { + var n = e.ref; + if (n === null) t !== null && t.ref !== null && (e.flags |= 4194816); + else { + if (typeof n != 'function' && typeof n != 'object') throw Error(r(284)); + (t === null || t.ref !== n) && (e.flags |= 4194816); + } + } + function Go(t, e, n, a, s) { + return ( + si(e), + (n = To(t, e, n, a, void 0, s)), + (a = Ao()), + t !== null && !Gt + ? (Co(t, e, s), rn(t, e, s)) + : (pt && a && io(e), (e.flags |= 1), ee(t, e, n, s), e.child) + ); + } + function Oh(t, e, n, a, s, u) { + return ( + si(e), + (e.updateQueue = null), + (n = Bd(e, a, n, s)), + Vd(t), + (a = Ao()), + t !== null && !Gt + ? (Co(t, e, u), rn(t, e, u)) + : (pt && a && io(e), (e.flags |= 1), ee(t, e, n, u), e.child) + ); + } + function kh(t, e, n, a, s) { + if ((si(e), e.stateNode === null)) { + var u = zi, + h = n.contextType; + (typeof h == 'object' && h !== null && (u = te(h)), + (u = new n(a, u)), + (e.memoizedState = u.state !== null && u.state !== void 0 ? u.state : null), + (u.updater = Ho), + (e.stateNode = u), + (u._reactInternals = e), + (u = e.stateNode), + (u.props = a), + (u.state = e.memoizedState), + (u.refs = {}), + po(e), + (h = n.contextType), + (u.context = typeof h == 'object' && h !== null ? te(h) : zi), + (u.state = e.memoizedState), + (h = n.getDerivedStateFromProps), + typeof h == 'function' && (Uo(e, n, h, a), (u.state = e.memoizedState)), + typeof n.getDerivedStateFromProps == 'function' || + typeof u.getSnapshotBeforeUpdate == 'function' || + (typeof u.UNSAFE_componentWillMount != 'function' && + typeof u.componentWillMount != 'function') || + ((h = u.state), + typeof u.componentWillMount == 'function' && u.componentWillMount(), + typeof u.UNSAFE_componentWillMount == 'function' && u.UNSAFE_componentWillMount(), + h !== u.state && Ho.enqueueReplaceState(u, u.state, null), + ka(e, a, u, s), + Oa(), + (u.state = e.memoizedState)), + typeof u.componentDidMount == 'function' && (e.flags |= 4194308), + (a = !0)); + } else if (t === null) { + u = e.stateNode; + var x = e.memoizedProps, + S = di(n, x); + u.props = S; + var E = u.context, + _ = n.contextType; + ((h = zi), typeof _ == 'object' && _ !== null && (h = te(_))); + var O = n.getDerivedStateFromProps; + ((_ = typeof O == 'function' || typeof u.getSnapshotBeforeUpdate == 'function'), + (x = e.pendingProps !== x), + _ || + (typeof u.UNSAFE_componentWillReceiveProps != 'function' && + typeof u.componentWillReceiveProps != 'function') || + ((x || E !== h) && jh(e, u, a, h)), + (Cn = !1)); + var D = e.memoizedState; + ((u.state = D), + ka(e, a, u, s), + Oa(), + (E = e.memoizedState), + x || D !== E || Cn + ? (typeof O == 'function' && (Uo(e, n, O, a), (E = e.memoizedState)), + (S = Cn || Sh(e, n, S, a, D, E, h)) + ? (_ || + (typeof u.UNSAFE_componentWillMount != 'function' && + typeof u.componentWillMount != 'function') || + (typeof u.componentWillMount == 'function' && u.componentWillMount(), + typeof u.UNSAFE_componentWillMount == 'function' && + u.UNSAFE_componentWillMount()), + typeof u.componentDidMount == 'function' && (e.flags |= 4194308)) + : (typeof u.componentDidMount == 'function' && (e.flags |= 4194308), + (e.memoizedProps = a), + (e.memoizedState = E)), + (u.props = a), + (u.state = E), + (u.context = h), + (a = S)) + : (typeof u.componentDidMount == 'function' && (e.flags |= 4194308), (a = !1))); + } else { + ((u = e.stateNode), + yo(t, e), + (h = e.memoizedProps), + (_ = di(n, h)), + (u.props = _), + (O = e.pendingProps), + (D = u.context), + (E = n.contextType), + (S = zi), + typeof E == 'object' && E !== null && (S = te(E)), + (x = n.getDerivedStateFromProps), + (E = typeof x == 'function' || typeof u.getSnapshotBeforeUpdate == 'function') || + (typeof u.UNSAFE_componentWillReceiveProps != 'function' && + typeof u.componentWillReceiveProps != 'function') || + ((h !== O || D !== S) && jh(e, u, a, S)), + (Cn = !1), + (D = e.memoizedState), + (u.state = D), + ka(e, a, u, s), + Oa()); + var L = e.memoizedState; + h !== O || D !== L || Cn || (t !== null && t.dependencies !== null && Fl(t.dependencies)) + ? (typeof x == 'function' && (Uo(e, n, x, a), (L = e.memoizedState)), + (_ = + Cn || + Sh(e, n, _, a, D, L, S) || + (t !== null && t.dependencies !== null && Fl(t.dependencies))) + ? (E || + (typeof u.UNSAFE_componentWillUpdate != 'function' && + typeof u.componentWillUpdate != 'function') || + (typeof u.componentWillUpdate == 'function' && u.componentWillUpdate(a, L, S), + typeof u.UNSAFE_componentWillUpdate == 'function' && + u.UNSAFE_componentWillUpdate(a, L, S)), + typeof u.componentDidUpdate == 'function' && (e.flags |= 4), + typeof u.getSnapshotBeforeUpdate == 'function' && (e.flags |= 1024)) + : (typeof u.componentDidUpdate != 'function' || + (h === t.memoizedProps && D === t.memoizedState) || + (e.flags |= 4), + typeof u.getSnapshotBeforeUpdate != 'function' || + (h === t.memoizedProps && D === t.memoizedState) || + (e.flags |= 1024), + (e.memoizedProps = a), + (e.memoizedState = L)), + (u.props = a), + (u.state = L), + (u.context = S), + (a = _)) + : (typeof u.componentDidUpdate != 'function' || + (h === t.memoizedProps && D === t.memoizedState) || + (e.flags |= 4), + typeof u.getSnapshotBeforeUpdate != 'function' || + (h === t.memoizedProps && D === t.memoizedState) || + (e.flags |= 1024), + (a = !1)); + } + return ( + (u = a), + ps(t, e), + (a = (e.flags & 128) !== 0), + u || a + ? ((u = e.stateNode), + (n = a && typeof n.getDerivedStateFromError != 'function' ? null : u.render()), + (e.flags |= 1), + t !== null && a + ? ((e.child = ci(e, t.child, null, s)), (e.child = ci(e, null, n, s))) + : ee(t, e, n, s), + (e.memoizedState = u.state), + (t = e.child)) + : (t = rn(t, e, s)), + t + ); + } + function Vh(t, e, n, a) { + return (ai(), (e.flags |= 256), ee(t, e, n, a), e.child); + } + var Xo = { dehydrated: null, treeContext: null, retryLane: 0, hydrationErrors: null }; + function Zo(t) { + return { baseLanes: t, cachePool: Md() }; + } + function Qo(t, e, n) { + return ((t = t !== null ? t.childLanes & ~n : 0), e && (t |= Ce), t); + } + function Bh(t, e, n) { + var a = e.pendingProps, + s = !1, + u = (e.flags & 128) !== 0, + h; + if ( + ((h = u) || (h = t !== null && t.memoizedState === null ? !1 : (Bt.current & 2) !== 0), + h && ((s = !0), (e.flags &= -129)), + (h = (e.flags & 32) !== 0), + (e.flags &= -33), + t === null) + ) { + if (pt) { + if ( + (s ? Dn(e) : wn(), + (t = wt) + ? ((t = Qm(t, ze)), + (t = t !== null && t.data !== '&' ? t : null), + t !== null && + ((e.memoizedState = { + dehydrated: t, + treeContext: Sn !== null ? { id: Ze, overflow: Qe } : null, + retryLane: 536870912, + hydrationErrors: null, + }), + (n = gd(t)), + (n.return = e), + (e.child = n), + (It = e), + (wt = null))) + : (t = null), + t === null) + ) + throw Tn(e); + return (Du(t) ? (e.lanes = 32) : (e.lanes = 536870912), null); + } + var x = a.children; + return ( + (a = a.fallback), + s + ? (wn(), + (s = e.mode), + (x = ys({ mode: 'hidden', children: x }, s)), + (a = ii(a, s, n, null)), + (x.return = e), + (a.return = e), + (x.sibling = a), + (e.child = x), + (a = e.child), + (a.memoizedState = Zo(n)), + (a.childLanes = Qo(t, h, n)), + (e.memoizedState = Xo), + Ya(null, a)) + : (Dn(e), Ko(e, x)) + ); + } + var S = t.memoizedState; + if (S !== null && ((x = S.dehydrated), x !== null)) { + if (u) + e.flags & 256 + ? (Dn(e), (e.flags &= -257), (e = Wo(t, e, n))) + : e.memoizedState !== null + ? (wn(), (e.child = t.child), (e.flags |= 128), (e = null)) + : (wn(), + (x = a.fallback), + (s = e.mode), + (a = ys({ mode: 'visible', children: a.children }, s)), + (x = ii(x, s, n, null)), + (x.flags |= 2), + (a.return = e), + (x.return = e), + (a.sibling = x), + (e.child = a), + ci(e, t.child, null, n), + (a = e.child), + (a.memoizedState = Zo(n)), + (a.childLanes = Qo(t, h, n)), + (e.memoizedState = Xo), + (e = Ya(null, a))); + else if ((Dn(e), Du(x))) { + if (((h = x.nextSibling && x.nextSibling.dataset), h)) var E = h.dgst; + ((h = E), + (a = Error(r(419))), + (a.stack = ''), + (a.digest = h), + wa({ value: a, source: null, stack: null }), + (e = Wo(t, e, n))); + } else if ((Gt || Vi(t, e, n, !1), (h = (n & t.childLanes) !== 0), Gt || h)) { + if (((h = Et), h !== null && ((a = Af(h, n)), a !== 0 && a !== S.retryLane))) + throw ((S.retryLane = a), ni(t, a), ye(h, t, a), qo); + (Eu(x) || Cs(), (e = Wo(t, e, n))); + } else + Eu(x) + ? ((e.flags |= 192), (e.child = t.child), (e = null)) + : ((t = S.treeContext), + (wt = Oe(x.nextSibling)), + (It = e), + (pt = !0), + (jn = null), + (ze = !1), + t !== null && bd(e, t), + (e = Ko(e, a.children)), + (e.flags |= 4096)); + return e; + } + return s + ? (wn(), + (x = a.fallback), + (s = e.mode), + (S = t.child), + (E = S.sibling), + (a = tn(S, { mode: 'hidden', children: a.children })), + (a.subtreeFlags = S.subtreeFlags & 65011712), + E !== null ? (x = tn(E, x)) : ((x = ii(x, s, n, null)), (x.flags |= 2)), + (x.return = e), + (a.return = e), + (a.sibling = x), + (e.child = a), + Ya(null, a), + (a = e.child), + (x = t.child.memoizedState), + x === null + ? (x = Zo(n)) + : ((s = x.cachePool), + s !== null + ? ((S = Yt._currentValue), (s = s.parent !== S ? { parent: S, pool: S } : s)) + : (s = Md()), + (x = { baseLanes: x.baseLanes | n, cachePool: s })), + (a.memoizedState = x), + (a.childLanes = Qo(t, h, n)), + (e.memoizedState = Xo), + Ya(t.child, a)) + : (Dn(e), + (n = t.child), + (t = n.sibling), + (n = tn(n, { mode: 'visible', children: a.children })), + (n.return = e), + (n.sibling = null), + t !== null && + ((h = e.deletions), h === null ? ((e.deletions = [t]), (e.flags |= 16)) : h.push(t)), + (e.child = n), + (e.memoizedState = null), + n); + } + function Ko(t, e) { + return ((e = ys({ mode: 'visible', children: e }, t.mode)), (e.return = t), (t.child = e)); + } + function ys(t, e) { + return ((t = Se(22, t, null, e)), (t.lanes = 0), t); + } + function Wo(t, e, n) { + return ( + ci(e, t.child, null, n), + (t = Ko(e, e.pendingProps.children)), + (t.flags |= 2), + (e.memoizedState = null), + t + ); + } + function Uh(t, e, n) { + t.lanes |= e; + var a = t.alternate; + (a !== null && (a.lanes |= e), oo(t.return, e, n)); + } + function Jo(t, e, n, a, s, u) { + var h = t.memoizedState; + h === null + ? (t.memoizedState = { + isBackwards: e, + rendering: null, + renderingStartTime: 0, + last: a, + tail: n, + tailMode: s, + treeForkCount: u, + }) + : ((h.isBackwards = e), + (h.rendering = null), + (h.renderingStartTime = 0), + (h.last = a), + (h.tail = n), + (h.tailMode = s), + (h.treeForkCount = u)); + } + function Hh(t, e, n) { + var a = e.pendingProps, + s = a.revealOrder, + u = a.tail; + a = a.children; + var h = Bt.current, + x = (h & 2) !== 0; + if ( + (x ? ((h = (h & 1) | 2), (e.flags |= 128)) : (h &= 1), + X(Bt, h), + ee(t, e, a, n), + (a = pt ? Da : 0), + !x && t !== null && (t.flags & 128) !== 0) + ) + t: for (t = e.child; t !== null; ) { + if (t.tag === 13) t.memoizedState !== null && Uh(t, n, e); + else if (t.tag === 19) Uh(t, n, e); + else if (t.child !== null) { + ((t.child.return = t), (t = t.child)); + continue; + } + if (t === e) break t; + for (; t.sibling === null; ) { + if (t.return === null || t.return === e) break t; + t = t.return; + } + ((t.sibling.return = t.return), (t = t.sibling)); + } + switch (s) { + case 'forwards': + for (n = e.child, s = null; n !== null; ) + ((t = n.alternate), t !== null && as(t) === null && (s = n), (n = n.sibling)); + ((n = s), + n === null ? ((s = e.child), (e.child = null)) : ((s = n.sibling), (n.sibling = null)), + Jo(e, !1, s, n, u, a)); + break; + case 'backwards': + case 'unstable_legacy-backwards': + for (n = null, s = e.child, e.child = null; s !== null; ) { + if (((t = s.alternate), t !== null && as(t) === null)) { + e.child = s; + break; + } + ((t = s.sibling), (s.sibling = n), (n = s), (s = t)); + } + Jo(e, !0, n, null, u, a); + break; + case 'together': + Jo(e, !1, null, null, void 0, a); + break; + default: + e.memoizedState = null; + } + return e.child; + } + function rn(t, e, n) { + if ( + (t !== null && (e.dependencies = t.dependencies), (_n |= e.lanes), (n & e.childLanes) === 0) + ) + if (t !== null) { + if ((Vi(t, e, n, !1), (n & e.childLanes) === 0)) return null; + } else return null; + if (t !== null && e.child !== t.child) throw Error(r(153)); + if (e.child !== null) { + for (t = e.child, n = tn(t, t.pendingProps), e.child = n, n.return = e; t.sibling !== null; ) + ((t = t.sibling), (n = n.sibling = tn(t, t.pendingProps)), (n.return = e)); + n.sibling = null; + } + return e.child; + } + function Fo(t, e) { + return (t.lanes & e) !== 0 ? !0 : ((t = t.dependencies), !!(t !== null && Fl(t))); + } + function ax(t, e, n) { + switch (e.tag) { + case 3: + (se(e, e.stateNode.containerInfo), An(e, Yt, t.memoizedState.cache), ai()); + break; + case 27: + case 5: + ha(e); + break; + case 4: + se(e, e.stateNode.containerInfo); + break; + case 10: + An(e, e.type, e.memoizedProps.value); + break; + case 31: + if (e.memoizedState !== null) return ((e.flags |= 128), So(e), null); + break; + case 13: + var a = e.memoizedState; + if (a !== null) + return a.dehydrated !== null + ? (Dn(e), (e.flags |= 128), null) + : (n & e.child.childLanes) !== 0 + ? Bh(t, e, n) + : (Dn(e), (t = rn(t, e, n)), t !== null ? t.sibling : null); + Dn(e); + break; + case 19: + var s = (t.flags & 128) !== 0; + if ( + ((a = (n & e.childLanes) !== 0), + a || (Vi(t, e, n, !1), (a = (n & e.childLanes) !== 0)), + s) + ) { + if (a) return Hh(t, e, n); + e.flags |= 128; + } + if ( + ((s = e.memoizedState), + s !== null && ((s.rendering = null), (s.tail = null), (s.lastEffect = null)), + X(Bt, Bt.current), + a) + ) + break; + return null; + case 22: + return ((e.lanes = 0), _h(t, e, n, e.pendingProps)); + case 24: + An(e, Yt, t.memoizedState.cache); + } + return rn(t, e, n); + } + function Yh(t, e, n) { + if (t !== null) + if (t.memoizedProps !== e.pendingProps) Gt = !0; + else { + if (!Fo(t, n) && (e.flags & 128) === 0) return ((Gt = !1), ax(t, e, n)); + Gt = (t.flags & 131072) !== 0; + } + else ((Gt = !1), pt && (e.flags & 1048576) !== 0 && vd(e, Da, e.index)); + switch (((e.lanes = 0), e.tag)) { + case 16: + t: { + var a = e.pendingProps; + if (((t = oi(e.elementType)), (e.type = t), typeof t == 'function')) + to(t) + ? ((a = di(t, a)), (e.tag = 1), (e = kh(null, e, t, a, n))) + : ((e.tag = 0), (e = Go(null, e, t, a, n))); + else { + if (t != null) { + var s = t.$$typeof; + if (s === Z) { + ((e.tag = 11), (e = wh(null, e, t, a, n))); + break t; + } else if (s === F) { + ((e.tag = 14), (e = Lh(null, e, t, a, n))); + break t; + } + } + throw ((e = Ht(t) || t), Error(r(306, e, ''))); + } + } + return e; + case 0: + return Go(t, e, e.type, e.pendingProps, n); + case 1: + return ((a = e.type), (s = di(a, e.pendingProps)), kh(t, e, a, s, n)); + case 3: + t: { + if ((se(e, e.stateNode.containerInfo), t === null)) throw Error(r(387)); + a = e.pendingProps; + var u = e.memoizedState; + ((s = u.element), yo(t, e), ka(e, a, null, n)); + var h = e.memoizedState; + if ( + ((a = h.cache), + An(e, Yt, a), + a !== u.cache && uo(e, [Yt], n, !0), + Oa(), + (a = h.element), + u.isDehydrated) + ) + if ( + ((u = { element: a, isDehydrated: !1, cache: h.cache }), + (e.updateQueue.baseState = u), + (e.memoizedState = u), + e.flags & 256) + ) { + e = Vh(t, e, a, n); + break t; + } else if (a !== s) { + ((s = Le(Error(r(424)), e)), wa(s), (e = Vh(t, e, a, n))); + break t; + } else + for ( + t = e.stateNode.containerInfo, + t.nodeType === 9 + ? (t = t.body) + : (t = t.nodeName === 'HTML' ? t.ownerDocument.body : t), + wt = Oe(t.firstChild), + It = e, + pt = !0, + jn = null, + ze = !0, + n = _d(e, null, a, n), + e.child = n; + n; + ) + ((n.flags = (n.flags & -3) | 4096), (n = n.sibling)); + else { + if ((ai(), a === s)) { + e = rn(t, e, n); + break t; + } + ee(t, e, a, n); + } + e = e.child; + } + return e; + case 26: + return ( + ps(t, e), + t === null + ? (n = $m(e.type, null, e.pendingProps, null)) + ? (e.memoizedState = n) + : pt || + ((n = e.type), + (t = e.pendingProps), + (a = _s(ut.current).createElement(n)), + (a[$t] = e), + (a[ce] = t), + ne(a, n, t), + Ft(a), + (e.stateNode = a)) + : (e.memoizedState = $m(e.type, t.memoizedProps, e.pendingProps, t.memoizedState)), + null + ); + case 27: + return ( + ha(e), + t === null && + pt && + ((a = e.stateNode = Jm(e.type, e.pendingProps, ut.current)), + (It = e), + (ze = !0), + (s = wt), + Vn(e.type) ? ((wu = s), (wt = Oe(a.firstChild))) : (wt = s)), + ee(t, e, e.pendingProps.children, n), + ps(t, e), + t === null && (e.flags |= 4194304), + e.child + ); + case 5: + return ( + t === null && + pt && + ((s = a = wt) && + ((a = Rx(a, e.type, e.pendingProps, ze)), + a !== null + ? ((e.stateNode = a), (It = e), (wt = Oe(a.firstChild)), (ze = !1), (s = !0)) + : (s = !1)), + s || Tn(e)), + ha(e), + (s = e.type), + (u = e.pendingProps), + (h = t !== null ? t.memoizedProps : null), + (a = u.children), + Au(s, u) ? (a = null) : h !== null && Au(s, h) && (e.flags |= 32), + e.memoizedState !== null && ((s = To(t, e, J1, null, null, n)), (nl._currentValue = s)), + ps(t, e), + ee(t, e, a, n), + e.child + ); + case 6: + return ( + t === null && + pt && + ((t = n = wt) && + ((n = Ox(n, e.pendingProps, ze)), + n !== null ? ((e.stateNode = n), (It = e), (wt = null), (t = !0)) : (t = !1)), + t || Tn(e)), + null + ); + case 13: + return Bh(t, e, n); + case 4: + return ( + se(e, e.stateNode.containerInfo), + (a = e.pendingProps), + t === null ? (e.child = ci(e, null, a, n)) : ee(t, e, a, n), + e.child + ); + case 11: + return wh(t, e, e.type, e.pendingProps, n); + case 7: + return (ee(t, e, e.pendingProps, n), e.child); + case 8: + return (ee(t, e, e.pendingProps.children, n), e.child); + case 12: + return (ee(t, e, e.pendingProps.children, n), e.child); + case 10: + return ((a = e.pendingProps), An(e, e.type, a.value), ee(t, e, a.children, n), e.child); + case 9: + return ( + (s = e.type._context), + (a = e.pendingProps.children), + si(e), + (s = te(s)), + (a = a(s)), + (e.flags |= 1), + ee(t, e, a, n), + e.child + ); + case 14: + return Lh(t, e, e.type, e.pendingProps, n); + case 15: + return Nh(t, e, e.type, e.pendingProps, n); + case 19: + return Hh(t, e, n); + case 31: + return ix(t, e, n); + case 22: + return _h(t, e, n, e.pendingProps); + case 24: + return ( + si(e), + (a = te(Yt)), + t === null + ? ((s = ho()), + s === null && + ((s = Et), + (u = co()), + (s.pooledCache = u), + u.refCount++, + u !== null && (s.pooledCacheLanes |= n), + (s = u)), + (e.memoizedState = { parent: a, cache: s }), + po(e), + An(e, Yt, s)) + : ((t.lanes & n) !== 0 && (yo(t, e), ka(e, null, null, n), Oa()), + (s = t.memoizedState), + (u = e.memoizedState), + s.parent !== a + ? ((s = { parent: a, cache: a }), + (e.memoizedState = s), + e.lanes === 0 && (e.memoizedState = e.updateQueue.baseState = s), + An(e, Yt, a)) + : ((a = u.cache), An(e, Yt, a), a !== s.cache && uo(e, [Yt], n, !0))), + ee(t, e, e.pendingProps.children, n), + e.child + ); + case 29: + throw e.pendingProps; + } + throw Error(r(156, e.tag)); + } + function on(t) { + t.flags |= 4; + } + function Po(t, e, n, a, s) { + if (((e = (t.mode & 32) !== 0) && (e = !1), e)) { + if (((t.flags |= 16777216), (s & 335544128) === s)) + if (t.stateNode.complete) t.flags |= 8192; + else if (mm()) t.flags |= 8192; + else throw ((ui = ts), mo); + } else t.flags &= -16777217; + } + function qh(t, e) { + if (e.type !== 'stylesheet' || (e.state.loading & 4) !== 0) t.flags &= -16777217; + else if (((t.flags |= 16777216), !ip(e))) + if (mm()) t.flags |= 8192; + else throw ((ui = ts), mo); + } + function gs(t, e) { + (e !== null && (t.flags |= 4), + t.flags & 16384 && ((e = t.tag !== 22 ? Sf() : 536870912), (t.lanes |= e), (Ji |= e))); + } + function qa(t, e) { + if (!pt) + switch (t.tailMode) { + case 'hidden': + e = t.tail; + for (var n = null; e !== null; ) (e.alternate !== null && (n = e), (e = e.sibling)); + n === null ? (t.tail = null) : (n.sibling = null); + break; + case 'collapsed': + n = t.tail; + for (var a = null; n !== null; ) (n.alternate !== null && (a = n), (n = n.sibling)); + a === null + ? e || t.tail === null + ? (t.tail = null) + : (t.tail.sibling = null) + : (a.sibling = null); + } + } + function Lt(t) { + var e = t.alternate !== null && t.alternate.child === t.child, + n = 0, + a = 0; + if (e) + for (var s = t.child; s !== null; ) + ((n |= s.lanes | s.childLanes), + (a |= s.subtreeFlags & 65011712), + (a |= s.flags & 65011712), + (s.return = t), + (s = s.sibling)); + else + for (s = t.child; s !== null; ) + ((n |= s.lanes | s.childLanes), + (a |= s.subtreeFlags), + (a |= s.flags), + (s.return = t), + (s = s.sibling)); + return ((t.subtreeFlags |= a), (t.childLanes = n), e); + } + function lx(t, e, n) { + var a = e.pendingProps; + switch ((ao(e), e.tag)) { + case 16: + case 15: + case 0: + case 11: + case 7: + case 8: + case 12: + case 9: + case 14: + return (Lt(e), null); + case 1: + return (Lt(e), null); + case 3: + return ( + (n = e.stateNode), + (a = null), + t !== null && (a = t.memoizedState.cache), + e.memoizedState.cache !== a && (e.flags |= 2048), + an(Yt), + Vt(), + n.pendingContext && ((n.context = n.pendingContext), (n.pendingContext = null)), + (t === null || t.child === null) && + (ki(e) + ? on(e) + : t === null || + (t.memoizedState.isDehydrated && (e.flags & 256) === 0) || + ((e.flags |= 1024), so())), + Lt(e), + null + ); + case 26: + var s = e.type, + u = e.memoizedState; + return ( + t === null + ? (on(e), u !== null ? (Lt(e), qh(e, u)) : (Lt(e), Po(e, s, null, a, n))) + : u + ? u !== t.memoizedState + ? (on(e), Lt(e), qh(e, u)) + : (Lt(e), (e.flags &= -16777217)) + : ((t = t.memoizedProps), t !== a && on(e), Lt(e), Po(e, s, t, a, n)), + null + ); + case 27: + if ((Dl(e), (n = ut.current), (s = e.type), t !== null && e.stateNode != null)) + t.memoizedProps !== a && on(e); + else { + if (!a) { + if (e.stateNode === null) throw Error(r(166)); + return (Lt(e), null); + } + ((t = $.current), ki(e) ? Sd(e) : ((t = Jm(s, a, n)), (e.stateNode = t), on(e))); + } + return (Lt(e), null); + case 5: + if ((Dl(e), (s = e.type), t !== null && e.stateNode != null)) + t.memoizedProps !== a && on(e); + else { + if (!a) { + if (e.stateNode === null) throw Error(r(166)); + return (Lt(e), null); + } + if (((u = $.current), ki(e))) Sd(e); + else { + var h = _s(ut.current); + switch (u) { + case 1: + u = h.createElementNS('http://www.w3.org/2000/svg', s); + break; + case 2: + u = h.createElementNS('http://www.w3.org/1998/Math/MathML', s); + break; + default: + switch (s) { + case 'svg': + u = h.createElementNS('http://www.w3.org/2000/svg', s); + break; + case 'math': + u = h.createElementNS('http://www.w3.org/1998/Math/MathML', s); + break; + case 'script': + ((u = h.createElement('div')), + (u.innerHTML = ' - +
diff --git a/app/src/CalculatorRouter.tsx b/app/src/CalculatorRouter.tsx index e28e04783..84f1b378c 100644 --- a/app/src/CalculatorRouter.tsx +++ b/app/src/CalculatorRouter.tsx @@ -6,14 +6,19 @@ import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom'; import PathwayLayout from './components/PathwayLayout'; import StandardLayout from './components/StandardLayout'; import DashboardPage from './pages/Dashboard.page'; +import PlaygroundPage from './pages/playground/PlaygroundPage'; import PoliciesPage from './pages/Policies.page'; +import PolicyEditingConceptPage from './pages/policyEditingConcepts/PolicyEditingConceptPage'; +import PolicyEditingConceptsPage from './pages/policyEditingConcepts/PolicyEditingConcepts.page'; import PopulationsPage from './pages/Populations.page'; +import ModifyReportPage from './pages/reportBuilder/ModifyReportPage'; +// Old monolithic file preserved but not used - see ./pages/ReportBuilder.page.tsx +import ReportBuilderPage from './pages/reportBuilder/ReportBuilderPage'; import ReportOutputPage from './pages/ReportOutput.page'; import ReportsPage from './pages/Reports.page'; import SimulationsPage from './pages/Simulations.page'; import PolicyPathwayWrapper from './pathways/policy/PolicyPathwayWrapper'; import PopulationPathwayWrapper from './pathways/population/PopulationPathwayWrapper'; -import ReportPathwayWrapper from './pathways/report/ReportPathwayWrapper'; import SimulationPathwayWrapper from './pathways/simulation/SimulationPathwayWrapper'; import { CountryGuard } from './routing/guards/CountryGuard'; import { MetadataGuard } from './routing/guards/MetadataGuard'; @@ -68,10 +73,6 @@ const router = createBrowserRouter( { element: , children: [ - { - path: 'reports/create', - element: , - }, { path: 'simulations/create', element: , @@ -119,6 +120,26 @@ const router = createBrowserRouter( path: 'policies', element: , }, + { + path: 'reports/create', + element: , + }, + { + path: 'reports/create/:userReportId', + element: , + }, + { + path: 'policy-editing-concepts', + element: , + }, + { + path: 'policy-editing-concepts/:conceptId', + element: , + }, + { + path: 'playground', + element: , + }, { path: 'account', element:
Account settings page
, diff --git a/app/src/api/societyWideCalculation.ts b/app/src/api/societyWideCalculation.ts index 61656b6b3..94018bc36 100644 --- a/app/src/api/societyWideCalculation.ts +++ b/app/src/api/societyWideCalculation.ts @@ -43,12 +43,18 @@ export async function fetchSocietyWideCalculation( }); if (!response.ok) { + let body = ''; + try { + body = await response.text(); + } catch { + // ignore + } console.error( - '[fetchSocietyWideCalculation] Failed with status:', - response.status, - response.statusText + `[fetchSocietyWideCalculation] ${response.status} ${response.statusText}`, + url, + body ); - throw new Error(`Society-wide calculation failed: ${response.statusText}`); + throw new Error(`Society-wide calculation failed (${response.status}): ${body || response.statusText}`); } const data = await response.json(); diff --git a/app/src/api/usageTracking.ts b/app/src/api/usageTracking.ts new file mode 100644 index 000000000..0b9ba0253 --- /dev/null +++ b/app/src/api/usageTracking.ts @@ -0,0 +1,103 @@ +/** + * Usage Tracking Store + * + * A lightweight system for tracking "last used" timestamps for any ingredient + * type (policies, households, geographies, etc.). + * + * This is separate from association data - it only tracks when items + * were last accessed, not the items themselves. + * + * Usage: + * import { policyUsageStore } from '@/api/usageTracking'; + * + * // Record that a policy was used + * policyUsageStore.recordUsage(policyId); + * + * // Get 5 most recently used policy IDs + * const recentIds = policyUsageStore.getRecentIds(5); + */ + +/** ISO timestamp string */ +export type UsageData = Record; + +/** + * Generic store for tracking usage of items by ID. + * Each ingredient type gets its own store instance with a unique storage key. + */ +export class UsageTrackingStore { + constructor(private readonly storageKey: string) {} + + /** + * Record that an item was used/accessed. + * Updates the lastUsedAt timestamp. + */ + recordUsage(id: string): string { + const usage = this.getAll(); + const timestamp = new Date().toISOString(); + usage[id] = timestamp; + localStorage.setItem(this.storageKey, JSON.stringify(usage)); + return timestamp; + } + + /** + * Get all usage records (id -> lastUsedAt timestamp). + */ + getAll(): UsageData { + try { + const stored = localStorage.getItem(this.storageKey); + return stored ? JSON.parse(stored) : {}; + } catch { + console.error(`[UsageTrackingStore] Failed to parse ${this.storageKey}`); + return {}; + } + } + + /** + * Get IDs sorted by most recently used. + * @param limit Maximum number of IDs to return (default 10) + */ + getRecentIds(limit = 10): string[] { + const usage = this.getAll(); + return Object.entries(usage) + .sort(([, a], [, b]) => b.localeCompare(a)) + .slice(0, limit) + .map(([id]) => id); + } + + /** + * Get the last used timestamp for a specific ID. + */ + getLastUsed(id: string): string | null { + return this.getAll()[id] || null; + } + + /** + * Check if an item has any usage recorded. + */ + hasUsage(id: string): boolean { + return !!this.getAll()[id]; + } + + /** + * Remove usage record for a specific ID. + */ + removeUsage(id: string): void { + const usage = this.getAll(); + delete usage[id]; + localStorage.setItem(this.storageKey, JSON.stringify(usage)); + } + + /** + * Clear all usage records for this store. + */ + clear(): void { + localStorage.removeItem(this.storageKey); + } +} + +// Pre-configured stores for each ingredient type +export const policyUsageStore = new UsageTrackingStore('policy-usage'); +export const householdUsageStore = new UsageTrackingStore('household-usage'); +export const geographyUsageStore = new UsageTrackingStore('geography-usage'); +export const simulationUsageStore = new UsageTrackingStore('simulation-usage'); +export const reportUsageStore = new UsageTrackingStore('report-usage'); diff --git a/app/src/components/IngredientReadView.tsx b/app/src/components/IngredientReadView.tsx index 8967f12d0..ab12dc201 100644 --- a/app/src/components/IngredientReadView.tsx +++ b/app/src/components/IngredientReadView.tsx @@ -1,5 +1,5 @@ import { IconPlus } from '@tabler/icons-react'; -import { Box, Button, Checkbox, Flex, Loader, Paper, Table, Text, Title } from '@mantine/core'; +import { Box, Button, Flex, Loader, Paper, Table, Text, Title } from '@mantine/core'; import { colors, spacing, typography } from '@/designTokens'; import { ColumnConfig, ColumnRenderer, IngredientRecord } from './columns'; import EmptyState from './common/EmptyState'; @@ -19,9 +19,6 @@ interface IngredientReadViewProps { searchValue?: string; onSearchChange?: (value: string) => void; onMoreFilters?: () => void; - enableSelection?: boolean; - isSelected?: (recordId: string) => boolean; - onSelectionChange?: (recordId: string, selected: boolean) => void; } export default function IngredientReadView({ @@ -38,9 +35,6 @@ export default function IngredientReadView({ searchValue: _searchValue = '', onSearchChange: _onSearchChange, onMoreFilters: _onMoreFilters, - enableSelection = true, - isSelected = () => false, - onSelectionChange, }: IngredientReadViewProps) { return ( @@ -115,16 +109,6 @@ export default function IngredientReadView({ - {enableSelection && ( - - {/* Optional: Add "select all" checkbox here in the future */} - - )} {columns.map((column) => ( - {data.map((record) => { - const selected = isSelected(record.id); - return ( - { - if (enableSelection && onSelectionChange) { - onSelectionChange(record.id, !selected); - } - }} - > - {enableSelection && ( - - { - event.stopPropagation(); - if (onSelectionChange) { - onSelectionChange(record.id, event.currentTarget.checked); - } - }} - size="sm" - /> - - )} - {columns.map((column) => ( - - - - ))} - - ); - })} + {data.map((record) => ( + + {columns.map((column) => ( + + + + ))} + + ))}
)} diff --git a/app/src/components/Sidebar.tsx b/app/src/components/Sidebar.tsx index 1c0fc0073..14e13ef60 100644 --- a/app/src/components/Sidebar.tsx +++ b/app/src/components/Sidebar.tsx @@ -8,7 +8,7 @@ import { IconMail, IconPlus, IconScale, - IconSettings2, + IconTestPipe, IconUsers, } from '@tabler/icons-react'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -35,6 +35,7 @@ export default function Sidebar({ isOpen = true }: SidebarProps) { { label: 'Simulations', icon: IconGitBranch, path: `/${countryId}/simulations` }, { label: 'Policies', icon: IconScale, path: `/${countryId}/policies` }, { label: 'Households', icon: IconUsers, path: `/${countryId}/households` }, + { label: 'Playground', icon: IconTestPipe, path: `/${countryId}/playground` }, ]; const resourceItems = [ @@ -74,12 +75,6 @@ export default function Sidebar({ isOpen = true }: SidebarProps) { path: 'mailto:hello@policyengine.org', external: true, }, - { - label: 'Account settings', - icon: IconSettings2, - path: `/${countryId}/account`, - disabled: true, - }, ]; if (!isOpen) { diff --git a/app/src/components/columns/ActionsColumn.tsx b/app/src/components/columns/ActionsColumn.tsx new file mode 100644 index 000000000..f249229ed --- /dev/null +++ b/app/src/components/columns/ActionsColumn.tsx @@ -0,0 +1,27 @@ +import { ActionIcon, Box, Tooltip } from '@mantine/core'; +import { spacing } from '@/designTokens'; +import { ActionsColumnConfig, IngredientRecord } from './types'; + +interface ActionsColumnProps { + config: ActionsColumnConfig; + record: IngredientRecord; +} + +export function ActionsColumn({ config, record }: ActionsColumnProps) { + return ( + + {config.actions.map((action) => ( + + config.onAction(action.action, record.id)} + > + {action.icon} + + + ))} + + ); +} diff --git a/app/src/components/columns/ColumnRenderer.tsx b/app/src/components/columns/ColumnRenderer.tsx index da563d60a..491a77209 100644 --- a/app/src/components/columns/ColumnRenderer.tsx +++ b/app/src/components/columns/ColumnRenderer.tsx @@ -1,11 +1,13 @@ import { Text } from '@mantine/core'; import { colors } from '@/designTokens'; +import { ActionsColumn } from './ActionsColumn'; import { BulletsColumn } from './BulletsColumn'; import { LinkColumn } from './LinkColumn'; import { MenuColumn } from './MenuColumn'; import { SplitMenuColumn } from './SplitMenuColumn'; import { TextColumn } from './TextColumn'; import { + ActionsColumnConfig, BulletsColumnConfig, BulletsValue, ColumnConfig, @@ -28,7 +30,12 @@ interface ColumnRendererProps { export function ColumnRenderer({ config, record }: ColumnRendererProps) { const value = record[config.key] as ColumnValue; - if (!value && config.type !== 'menu' && config.type !== 'split-menu') { + if ( + !value && + config.type !== 'menu' && + config.type !== 'split-menu' && + config.type !== 'actions' + ) { return ( — @@ -57,6 +64,9 @@ export function ColumnRenderer({ config, record }: ColumnRendererProps) { case 'split-menu': return ; + case 'actions': + return ; + default: return {String(value)}; } diff --git a/app/src/components/columns/index.ts b/app/src/components/columns/index.ts index 837f25e5d..a3a034fdd 100644 --- a/app/src/components/columns/index.ts +++ b/app/src/components/columns/index.ts @@ -2,6 +2,7 @@ export * from './types'; // Export column components +export { ActionsColumn } from './ActionsColumn'; export { ColumnRenderer } from './ColumnRenderer'; export { TextColumn } from './TextColumn'; export { LinkColumn } from './LinkColumn'; diff --git a/app/src/components/columns/types.ts b/app/src/components/columns/types.ts index f708b11b9..61079713f 100644 --- a/app/src/components/columns/types.ts +++ b/app/src/components/columns/types.ts @@ -48,12 +48,24 @@ export interface SplitMenuColumnConfig extends BaseColumnConfig { onAction: (action: string, recordId: string) => void; } +export interface ActionsColumnConfig extends BaseColumnConfig { + type: 'actions'; + actions: Array<{ + action: string; + tooltip: string; + icon: React.ReactNode; + color?: string; + }>; + onAction: (action: string, recordId: string) => void; +} + export type ColumnConfig = | TextColumnConfig | LinkColumnConfig | BulletsColumnConfig | MenuColumnConfig - | SplitMenuColumnConfig; + | SplitMenuColumnConfig + | ActionsColumnConfig; // Data value interfaces export interface TextValue { diff --git a/app/src/components/common/ActionButtons.tsx b/app/src/components/common/ActionButtons.tsx new file mode 100644 index 000000000..e5f408133 --- /dev/null +++ b/app/src/components/common/ActionButtons.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { + IconInfoCircle, + IconNewSection, + IconPencil, + IconShare, + IconStatusChange, + IconTransfer, +} from '@tabler/icons-react'; +import { ActionIcon, Button, Tooltip } from '@mantine/core'; +import { colors, typography } from '@/designTokens'; + +export interface ActionButtonProps { + label?: string; + tooltip?: string; + onClick?: () => void; + disabled?: boolean; + loading?: boolean; + color?: string; + variant?: string; + size?: string; + tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'; +} + +interface ActionButtonBaseProps extends ActionButtonProps { + icon: React.ComponentType<{ size: number }>; + tooltip: string; +} + +function ActionButtonBase({ + icon: Icon, + tooltip, + label, + onClick, + disabled, + loading, + color, + variant = 'light', + size = 'lg', + tooltipPosition = 'bottom', +}: ActionButtonBaseProps) { + const tooltipStyles = { + tooltip: { + backgroundColor: colors.gray[700], + fontSize: typography.fontSize.xs, + }, + }; + + if (label) { + return ( + + + + ); + } + + return ( + + + + + + ); +} + +export function ViewButton({ tooltip: tooltipOverride, ...props }: ActionButtonProps) { + return ; +} + +export function EditAndUpdateButton(props: ActionButtonProps) { + return ; +} + +export function EditAndSaveNewButton(props: ActionButtonProps) { + return ; +} + +export function EditDefaultButton(props: ActionButtonProps) { + return ; +} + +export function ShareButton(props: ActionButtonProps) { + return ; +} + +export function SwapButton(props: ActionButtonProps) { + return ; +} diff --git a/app/src/components/icons/CountryOutlineIcons.tsx b/app/src/components/icons/CountryOutlineIcons.tsx new file mode 100644 index 000000000..053b4c9d8 --- /dev/null +++ b/app/src/components/icons/CountryOutlineIcons.tsx @@ -0,0 +1,173 @@ +/** + * Country outline icons for US and UK + * Source: https://github.com/djaiss/mapsicon (MIT License) + * These icons use currentColor for fill, so they inherit text color from parent. + */ + +interface CountryIconProps { + size?: number; + color?: string; + className?: string; + style?: React.CSSProperties; +} + +export function USOutlineIcon({ size = 24, color, className, style }: CountryIconProps) { + return ( + + + + + + + ); +} + +export function UKOutlineIcon({ size = 24, color, className, style }: CountryIconProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/src/components/report/DashboardCard.tsx b/app/src/components/report/DashboardCard.tsx new file mode 100644 index 000000000..54cf5b18a --- /dev/null +++ b/app/src/components/report/DashboardCard.tsx @@ -0,0 +1,400 @@ +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { IconArrowsMinimize, IconMaximize } from '@tabler/icons-react'; +import { motion } from 'framer-motion'; +import { ActionIcon, Box, Group, Stack, Text, Tooltip } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; + +const SECONDARY_ICON_SIZE = 36; +const FADE_MS = 150; +const RESIZE_S = 0.35; +export const SHRUNKEN_CARD_HEIGHT = 200; + +export type ExpandDirection = 'down-right' | 'down-left' | 'up-right' | 'up-left'; + +interface DashboardCardProps { + mode: 'expanded' | 'shrunken'; + zIndex: number; + expandDirection: ExpandDirection; + expandedContent: React.ReactNode; + onToggleMode?: () => void; + gridGap?: number; + + // Standard header mode (icon + label + slides) + icon?: React.ComponentType<{ size: number; color: string; stroke: number }>; + label?: string; + slides?: React.ReactNode[]; + + // Custom shrunken content (replaces header + slides entirely) + shrunkenContent?: React.ReactNode; + + // Two-part shrunken layout: header at top, body centered in remaining space + shrunkenHeader?: React.ReactNode; + shrunkenBody?: React.ReactNode; + + // Layout + colSpan?: number; + /** Number of grid rows the card occupies when expanded (default 2) */ + expandedRows?: number; + + /** Controls (e.g. SegmentedControl) rendered in a row with the minimize button when expanded */ + expandedControls?: React.ReactNode; + + // Style overrides (apply only when shrunken/idle) + shrunkenBackground?: string; + shrunkenBorderColor?: string; + padding?: string; +} + +const ANCHOR: Record = { + 'down-right': { top: 0, left: 0 }, + 'down-left': { top: 0, right: 0 }, + 'up-right': { bottom: 0, left: 0 }, + 'up-left': { bottom: 0, right: 0 }, +}; + +const RESIZE_TRANSITION = { + duration: RESIZE_S, + ease: [0.4, 0, 0.2, 1] as const, +}; + +type Phase = + | 'idle' // shrunken, relative, content visible + | 'pre-expand' // shrunken content fading out + | 'expanding' // card resizing to expanded dimensions + | 'expanded' // at expanded size, expanded content visible + | 'pre-collapse' // expanded content fading out + | 'collapsing'; // card resizing back to cell dimensions + +export default function DashboardCard({ + mode, + zIndex, + expandDirection, + expandedContent, + onToggleMode, + gridGap = 16, + icon: Icon, + label, + slides, + shrunkenContent, + shrunkenHeader, + shrunkenBody, + colSpan = 1, + expandedRows = 2, + expandedControls, + shrunkenBackground, + shrunkenBorderColor, + padding: paddingProp, +}: DashboardCardProps) { + const [activeSlideIndex, setActiveSlideIndex] = useState(0); + const cardRef = useRef(null); + const safeIndex = slides ? Math.min(activeSlideIndex, slides.length - 1) : 0; + const isExpanded = mode === 'expanded'; + const useCustomContent = !!shrunkenContent || !!shrunkenHeader; + const cardPadding = paddingProp ?? spacing.lg; + + const [phase, setPhase] = useState('idle'); + const [cell, setCell] = useState<{ w: number; h: number } | null>(null); + // Controls the 1-frame-delayed fade-in of expanded content + const [expandedVisible, setExpandedVisible] = useState(false); + + // --- Detect mode changes and drive phase transitions --- + useLayoutEffect(() => { + if (mode === 'expanded') { + if (phase === 'idle') { + // Measure cell while card is still in flow + if (cardRef.current) { + setCell({ + w: cardRef.current.offsetWidth, + h: cardRef.current.offsetHeight, + }); + } + setPhase('pre-expand'); + } else if (phase === 'collapsing' || phase === 'pre-collapse') { + // Reverse a collapse mid-animation + setPhase('expanding'); + } + } + if (mode === 'shrunken') { + if (phase === 'expanded') { + setPhase('pre-collapse'); + } else if (phase === 'pre-expand' || phase === 'expanding') { + // Reverse an expand mid-animation + setPhase('collapsing'); + } + } + }, [mode, phase]); + + // --- Timed fade phases (pre-expand / pre-collapse) --- + useEffect(() => { + if (phase === 'pre-expand') { + const t = setTimeout(() => setPhase('expanding'), FADE_MS); + return () => clearTimeout(t); + } + if (phase === 'pre-collapse') { + const t = setTimeout(() => setPhase('collapsing'), FADE_MS); + return () => clearTimeout(t); + } + }, [phase]); + + // --- Expanded content: mount with opacity 0, then fade in after 1 frame --- + useEffect(() => { + if (phase === 'expanded') { + const raf = requestAnimationFrame(() => { + setExpandedVisible(true); + }); + return () => cancelAnimationFrame(raf); + } + setExpandedVisible(false); + }, [phase]); + + // --- Trigger Plotly resize after expanded content becomes visible --- + useEffect(() => { + if (expandedVisible) { + const t = setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 50); + return () => clearTimeout(t); + } + }, [expandedVisible]); + + // Note: no need to clear framer-motion inline styles on return to idle — + // the collapse animation targets cell.h (= SHRUNKEN_CARD_HEIGHT), so the + // residual inline height is already correct. Clearing here would also fire + // on initial mount, wiping out React's height before the first paint. + + // --- Resize animation complete handler --- + const handleAnimationComplete = () => { + if (phase === 'expanding') { + setPhase('expanded'); + } else if (phase === 'collapsing') { + setPhase('idle'); + setCell(null); + window.dispatchEvent(new Event('resize')); + } + }; + + // --- Derived values --- + const isLifted = phase !== 'idle'; + const expandedW = colSpan >= 2 ? (cell?.w ?? 0) : cell ? cell.w * 2 + gridGap : 0; + const expandedH = cell ? cell.h * expandedRows + gridGap * (expandedRows - 1) : 0; + + // Background/border: use overrides only when idle (shrunken) + const cardBackground = + !isLifted && shrunkenBackground ? shrunkenBackground : colors.background.primary; + const cardBorderColor = + !isLifted && shrunkenBorderColor ? shrunkenBorderColor : colors.border.light; + + const shrunkenContentOpacity = phase === 'idle' ? 1 : 0; + const mountExpanded = phase === 'expanded' || phase === 'pre-collapse'; + const expandedContentOpacity = phase === 'expanded' && expandedVisible ? 1 : 0; + + // Animate target: cell size when shrinking, expanded size when growing + const getAnimateTarget = (): { width: number; height: number } | undefined => { + if (!cell) { + return undefined; + } + if (phase === 'expanding' || phase === 'expanded' || phase === 'pre-collapse') { + return { width: expandedW, height: expandedH }; + } + // pre-expand, collapsing + return { width: cell.w, height: cell.h }; + }; + + const anchor = ANCHOR[expandDirection]; + + const expandButton = onToggleMode ? ( + + { + e.stopPropagation(); + onToggleMode(); + }} + > + {isExpanded ? : } + + + ) : null; + + return ( + /* Wrapper: stays in grid flow, fixed height always */ +
1 ? { gridColumn: `span ${colSpan}` } : {}), + }} + > + + {/* Standard header — only when not using custom content */} + {!useCustomContent && Icon && label && ( + + + + + + + {label} + + + {expandButton} + + )} + + {/* Content area */} +
+ {/* Shrunken layer — always mounted, opacity-controlled */} +
+ {shrunkenHeader ? ( +
+
{shrunkenHeader}
+
+
{shrunkenBody}
+
+
+ ) : useCustomContent ? ( +
{shrunkenContent}
+ ) : ( + + {slides?.[safeIndex]} + {slides && slides.length > 1 && ( + + {slides.map((_, index) => ( + { + e.stopPropagation(); + setActiveSlideIndex(index); + }} + style={{ + width: 8, + height: 8, + borderRadius: '50%', + backgroundColor: + index === safeIndex ? colors.primary[500] : colors.gray[300], + cursor: 'pointer', + transition: 'background-color 0.2s ease', + }} + /> + ))} + + )} + + )} +
+ + {/* Expanded layer — only mounted after card finishes expanding */} + {mountExpanded && ( +
+ {/* Controls row: expandedControls on left, minimize button on right */} + {onToggleMode && ( +
+
+ {expandedControls} +
+ {expandButton} +
+ )} +
+ {expandedContent} +
+
+ )} +
+ + {/* Expand button for custom content mode — absolutely positioned (hidden when expanded) */} + {useCustomContent && expandButton && !mountExpanded && ( +
{expandButton}
+ )} +
+
+ ); +} diff --git a/app/src/components/report/MetricCard.tsx b/app/src/components/report/MetricCard.tsx index c8f358088..6ff77cbe7 100644 --- a/app/src/components/report/MetricCard.tsx +++ b/app/src/components/report/MetricCard.tsx @@ -1,12 +1,12 @@ -import { IconArrowDown, IconArrowUp, IconMinus } from '@tabler/icons-react'; +import { IconAlertTriangle, IconArrowDown, IconArrowUp, IconMinus } from '@tabler/icons-react'; import { Box, Group, Text, ThemeIcon } from '@mantine/core'; import { colors, spacing, typography } from '@/designTokens'; -type MetricTrend = 'positive' | 'negative' | 'neutral'; +type MetricTrend = 'positive' | 'negative' | 'neutral' | 'error'; interface MetricCardProps { - /** Label describing the metric */ - label: string; + /** Label describing the metric (omit to hide) */ + label?: string; /** The main value to display */ value: string; /** Optional secondary value or context */ @@ -42,12 +42,18 @@ export default function MetricCard({ return colors.primary[600]; case 'negative': return colors.gray[600]; + case 'error': + return 'rgb(220, 53, 69)'; default: return colors.gray[500]; } }; const getTrendIcon = () => { + if (trend === 'error') { + return ; + } + // When invertArrow is true, flip the arrow direction // (useful for metrics like poverty where decrease is good) const showUpArrow = invertArrow ? trend === 'negative' : trend === 'positive'; @@ -67,15 +73,17 @@ export default function MetricCard({ return ( {/* Label */} - - {label} - + {label && ( + + {label} + + )} {/* Value with trend indicator */} @@ -84,7 +92,7 @@ export default function MetricCard({ size={hero ? 32 : 24} radius="xl" variant="light" - color={trend === 'positive' ? 'teal' : 'gray'} + color={trend === 'positive' ? 'teal' : trend === 'error' ? 'red' : 'gray'} > {getTrendIcon()} diff --git a/app/src/components/report/ReportActionButtons.tsx b/app/src/components/report/ReportActionButtons.tsx index c99ddc089..46fef5698 100644 --- a/app/src/components/report/ReportActionButtons.tsx +++ b/app/src/components/report/ReportActionButtons.tsx @@ -1,26 +1,29 @@ -import { IconBookmark, IconPencil, IconShare } from '@tabler/icons-react'; -import { ActionIcon, Tooltip } from '@mantine/core'; +import { IconBookmark, IconCode, IconSettings } from '@tabler/icons-react'; +import { ActionIcon, Group, Tooltip } from '@mantine/core'; +import { ShareButton } from '@/components/common/ActionButtons'; import { colors, typography } from '@/designTokens'; interface ReportActionButtonsProps { isSharedView: boolean; onShare?: () => void; onSave?: () => void; - onEdit?: () => void; + onView?: () => void; + onReproduce?: () => void; } /** * ReportActionButtons - Action buttons for report output header * * Renders different buttons based on view type: - * - Normal view: Share + Edit buttons + * - Normal view: Reproduce + View/edit + Share buttons * - Shared view: Save button with tooltip */ export function ReportActionButtons({ isSharedView, onShare, onSave, - onEdit, + onView, + onReproduce, }: ReportActionButtonsProps) { if (isSharedView) { return ( @@ -47,26 +50,38 @@ export function ReportActionButtons({ ); } + const tooltipStyles = { + tooltip: { + backgroundColor: colors.gray[700], + fontSize: typography.fontSize.xs, + }, + }; + return ( - <> - - - - - - - + + + + + + + + + + + + + ); } diff --git a/app/src/components/visualization/choropleth/USDistrictChoroplethMap.tsx b/app/src/components/visualization/choropleth/USDistrictChoroplethMap.tsx index c1019ba6b..e1ef34626 100644 --- a/app/src/components/visualization/choropleth/USDistrictChoroplethMap.tsx +++ b/app/src/components/visualization/choropleth/USDistrictChoroplethMap.tsx @@ -108,6 +108,7 @@ export function USDistrictChoroplethMap({ config = {}, geoDataPath = DEFAULT_GEOJSON_PATH, focusState, + errorStates, }: USDistrictChoroplethMapProps) { // Load GeoJSON data const { geoJSON, loading, error } = useGeoJSONLoader(geoDataPath); @@ -129,8 +130,8 @@ export function USDistrictChoroplethMap({ if (!geoJSON) { return { plotData: [], plotLayout: {} }; } - return buildPlotDataAndLayout(geoJSON, dataMap, colorRange, fullConfig, focusState); - }, [geoJSON, dataMap, colorRange, fullConfig, focusState]); + return buildPlotDataAndLayout(geoJSON, dataMap, colorRange, fullConfig, focusState, errorStates); + }, [geoJSON, dataMap, colorRange, fullConfig, focusState, errorStates]); // Build Plotly config const plotConfig = useMemo(() => buildPlotConfig(), []); @@ -158,18 +159,10 @@ export function USDistrictChoroplethMap({ ); } - // No data state - if (!data.length) { - return ( -
- No district data available -
- ); - } - return ( { + const locations: string[] = []; + const features: GeoJSONFeature[] = []; + + geoJSON.features.forEach((feature: GeoJSONFeature) => { + const districtId = feature.properties?.DISTRICT_ID as string; + if (!districtId) return; + locations.push(districtId); + features.push(feature); + }); + + const bgGeoJSON: GeoJSONFeatureCollection = { + ...geoJSON, + features: features.map((f, i) => ({ ...f, id: locations[i] })), + }; + + return { + type: 'choropleth', + geojson: bgGeoJSON, + locations, + z: locations.map(() => 0), + featureidkey: 'id', + locationmode: 'geojson-id', + colorscale: [ + [0, colors.background.primary], + [1, colors.background.primary], + ] as ColorscaleEntry[], + zmin: 0, + zmax: 1, + showscale: false, + hoverinfo: 'skip', + marker: { + line: { + color: colors.gray[300], + width: 0.5, + }, + }, + } as Partial; +} + +/** + * Build a red error trace for districts belonging to states that failed to load. + * Shows a solid red fill with an error message on hover. + */ +export function buildErrorTrace( + geoJSON: GeoJSONFeatureCollection, + errorStates: string[] +): Partial | null { + if (errorStates.length === 0) { + return null; + } + + const errorSet = new Set(errorStates); + const locations: string[] = []; + const hoverText: string[] = []; + const features: GeoJSONFeature[] = []; + + geoJSON.features.forEach((feature: GeoJSONFeature) => { + const districtId = feature.properties?.DISTRICT_ID as string; + if (!districtId) { + return; + } + // DISTRICT_ID format: "CO-01" — extract 2-letter state abbreviation + const stateAbbr = districtId.split('-')[0]; + if (!errorSet.has(stateAbbr)) { + return; + } + locations.push(districtId); + hoverText.push( + `${feature.properties?.NAMELSAD ?? districtId}
Error loading data` + ); + features.push(feature); + }); + + if (locations.length === 0) { + return null; + } + + const errorGeoJSON: GeoJSONFeatureCollection = { + ...geoJSON, + features: features.map((f, i) => ({ ...f, id: locations[i] })), + }; + + return { + type: 'choropleth', + geojson: errorGeoJSON, + locations, + z: locations.map(() => 1), + text: hoverText, + featureidkey: 'id', + locationmode: 'geojson-id', + colorscale: [ + [0, 'rgba(220, 53, 69, 0.5)'], + [1, 'rgba(220, 53, 69, 0.5)'], + ] as ColorscaleEntry[], + zmin: 0, + zmax: 1, + showscale: false, + hovertemplate: '%{text}', + hoverlabel: { + bgcolor: 'rgba(220, 53, 69, 0.9)', + font: { color: 'white' }, + bordercolor: 'rgba(220, 53, 69, 1)', + }, + marker: { + line: { + color: 'rgba(220, 53, 69, 0.8)', + width: 1.0, + }, + }, + } as Partial; +} + /** * Build the geo configuration for Plotly. * @@ -273,8 +392,12 @@ export function buildPlotDataAndLayout( dataMap: Map, colorRange: ColorRange, config: ChoroplethMapConfig, - focusState?: string + focusState?: string, + errorStates?: string[] ): PlotDataAndLayout { + // Background trace: all district outlines (white fill, gray borders) + const backgroundTrace = buildBackgroundTrace(geoJSON); + // Process features to extract only those with data const processedData = processGeoJSONFeatures(geoJSON, dataMap, config.formatValue); @@ -287,9 +410,18 @@ export function buildPlotDataAndLayout( // Build geo config const geoConfig = buildGeoConfig(focusState); - // Build plot data and layout - const plotData = buildPlotData(geoJSONWithIds, processedData, colorscale, colorRange, config); + // Build data trace (colored fills for districts with data) + const dataTraces = buildPlotData(geoJSONWithIds, processedData, colorscale, colorRange, config); + + // Build error trace (red fill for districts in errored states) + const errorTrace = errorStates ? buildErrorTrace(geoJSON, errorStates) : null; + const plotLayout = buildPlotLayout(geoConfig, config.height); + const plotData = [backgroundTrace, ...dataTraces]; + if (errorTrace) { + plotData.push(errorTrace); + } + return { plotData, plotLayout }; } diff --git a/app/src/contexts/congressional-district/CongressionalDistrictDataContext.tsx b/app/src/contexts/congressional-district/CongressionalDistrictDataContext.tsx index dab06efb4..fc05e7389 100644 --- a/app/src/contexts/congressional-district/CongressionalDistrictDataContext.tsx +++ b/app/src/contexts/congressional-district/CongressionalDistrictDataContext.tsx @@ -226,6 +226,7 @@ export function CongressionalDistrictDataProvider({ isLoading, hasStarted: state.hasStarted, errorCount, + erroredStates: state.erroredStates, labelLookup, isStateLevelReport, stateCode: stateCodeValue, @@ -237,6 +238,7 @@ export function CongressionalDistrictDataProvider({ [ state.stateResponses, state.hasStarted, + state.erroredStates, completedCount, loadingCount, totalDistrictsLoaded, diff --git a/app/src/contexts/congressional-district/types.ts b/app/src/contexts/congressional-district/types.ts index bec1b5c98..e5fcfd79b 100644 --- a/app/src/contexts/congressional-district/types.ts +++ b/app/src/contexts/congressional-district/types.ts @@ -66,6 +66,8 @@ export interface CongressionalDistrictDataContextValue { hasStarted: boolean; /** Number of states that errored */ errorCount: number; + /** Set of state codes that errored (e.g., 'state/co') */ + erroredStates: Set; /** Label lookup for district display names */ labelLookup: DistrictLabelLookup; /** Whether this is a state-level report (single state) vs national */ diff --git a/app/src/data/posts/articles/sc-h3492-eitc-refundability.md b/app/src/data/posts/articles/sc-h3492-eitc-refundability.md index 8b75316a8..8879fb61f 100644 --- a/app/src/data/posts/articles/sc-h3492-eitc-refundability.md +++ b/app/src/data/posts/articles/sc-h3492-eitc-refundability.md @@ -25,16 +25,15 @@ Consider a single parent with one child (age 5) earning $20,000. Under current l **Table 1: SC tax provisions reducing liability for head of household filer with one young child** -| Provision | 2026 value | -| --- | --- | -| Head of household standard deduction | $24,150 | -| Dependent exemption | $5,040 | -| Young child exemption | $5,040 | -| 0% bracket threshold | $3,640 | -| **Total** | **$37,870** | - -As earnings rise above $37,870, the household begins to benefit from the nonrefundable EITC. For instance, if the household earned $45,000, they would owe $216 in state taxes. This reduces the "excess" credit available for refund, but net income still increases by $273, making their total EITC benefit $489 under House Bill 3492. At roughly $50,000, the household's tax liability exceeds the EITC's value, meaning there is no longer any excess and the bill provides no additional value. The SC EITC fully phases out near $52,000 of earnings for this family. - +| Provision | 2026 value | +| ------------------------------------ | ----------- | +| Head of household standard deduction | $24,150 | +| Dependent exemption | $5,040 | +| Young child exemption | $5,040 | +| 0% bracket threshold | $3,640 | +| **Total** | **$37,870** | + +As earnings rise above $37,870, the household begins to benefit from the nonrefundable EITC. For instance, if the household earned $45,000, they would owe $216 in state taxes. This reduces the "excess" credit available for refund, but net income still increases by $273, making their total EITC benefit $489 under House Bill 3492. At roughly $50,000, the household's tax liability exceeds the EITC's value, meaning there is no longer any excess and the bill provides no additional value. The SC EITC fully phases out near $52,000 of earnings for this family. Figure 1 displays the total SC EITC benefit that a single parent with one child would receive under current law and H.3492 as employment earnings rise. diff --git a/app/src/hooks/useUserReports.ts b/app/src/hooks/useUserReports.ts index cb77875a2..057541890 100644 --- a/app/src/hooks/useUserReports.ts +++ b/app/src/hooks/useUserReports.ts @@ -20,6 +20,7 @@ import { } from '@/types/ingredients/UserPopulation'; import { UserReport } from '@/types/ingredients/UserReport'; import { UserSimulation } from '@/types/ingredients/UserSimulation'; +import { HouseholdMetadata } from '@/types/metadata/householdMetadata'; import { findPlaceFromRegionString, getPlaceDisplayName } from '@/utils/regionStrategies'; import { householdKeys, policyKeys, reportKeys, simulationKeys } from '../libs/queryKeys'; import { useGeographicAssociationsByUser } from './useUserGeographic'; @@ -442,17 +443,16 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo const householdSimulations = simulations.filter((s) => s.populationType === 'household'); const householdIds = extractUniqueIds(householdSimulations, 'populationId'); - const householdResults = useParallelQueries(householdIds, { + const householdResults = useParallelQueries(householdIds, { queryKey: householdKeys.byId, - queryFn: async (id) => { - const metadata = await fetchHouseholdById(country, id); - return HouseholdAdapter.fromMetadata(metadata); - }, + queryFn: async (id) => fetchHouseholdById(country, id), enabled: isEnabled && householdIds.length > 0, staleTime: 5 * 60 * 1000, }); - const households = householdResults.queries.map((q) => q.data).filter((h): h is Household => !!h); + const households = householdResults.queries + .map((q) => (q.data ? HouseholdAdapter.fromMetadata(q.data) : undefined)) + .filter((h): h is Household => !!h); const userHouseholds = householdAssociations?.filter((ha) => households.some((h) => h.id === ha.householdId) diff --git a/app/src/libs/metadataUtils.ts b/app/src/libs/metadataUtils.ts index 881a1ec9a..caa390a8a 100644 --- a/app/src/libs/metadataUtils.ts +++ b/app/src/libs/metadataUtils.ts @@ -1,6 +1,15 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from '@/store'; import { MetadataApiPayload, MetadataState } from '@/types/metadata'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; + +/** Parameter paths containing these substrings are excluded from search */ +const EXCLUDED_PARAMETER_PATTERNS = ['pycache'] as const; + +export interface SearchableParameter { + value: string; // Full parameter path (e.g., "gov.irs.credits.eitc.max") + label: string; // Leaf label (e.g., "Maximum amount") +} // Memoized selectors to prevent unnecessary re-renders export const getTaxYears = createSelector( @@ -163,6 +172,30 @@ export const getFieldLabel = (fieldName: string) => { ); }; +/** + * Memoized selector for searchable parameters used in autocomplete components. + * Computed once when metadata loads, shared across all components. + */ +export const selectSearchableParameters = createSelector( + [(state: RootState) => state.metadata.parameters], + (parameters): SearchableParameter[] => { + if (!parameters) return []; + + return Object.values(parameters) + .filter( + (param): param is ParameterMetadata => + param.type === 'parameter' && + !!param.label && + !EXCLUDED_PARAMETER_PATTERNS.some((pattern) => param.parameter.includes(pattern)) + ) + .map((param) => ({ + value: param.parameter, + label: param.label, + })) + .sort((a, b) => a.label.localeCompare(b.label)); + } +); + export function transformMetadataPayload( payload: MetadataApiPayload, country: string diff --git a/app/src/pages/Policies.page.tsx b/app/src/pages/Policies.page.tsx index 4e37fda92..34a074531 100644 --- a/app/src/pages/Policies.page.tsx +++ b/app/src/pages/Policies.page.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { IconSettings } from '@tabler/icons-react'; import { useNavigate } from 'react-router-dom'; import { Stack } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; @@ -8,6 +9,9 @@ import IngredientReadView from '@/components/IngredientReadView'; import { MOCK_USER_ID } from '@/constants'; import { useCurrentCountry } from '@/hooks/useCurrentCountry'; import { useUpdatePolicyAssociation, useUserPolicies } from '@/hooks/useUserPolicy'; +import type { EditorMode } from '@/pages/reportBuilder/modals/policyCreation/types'; +import { PolicyCreationModal } from '@/pages/reportBuilder/modals/PolicyCreationModal'; +import { PolicyStateProps } from '@/types/pathwayState'; import { countPolicyModifications } from '@/utils/countParameterChanges'; import { formatDate } from '@/utils/dateUtils'; @@ -18,13 +22,18 @@ export default function PoliciesPage() { const countryId = useCurrentCountry(); const [searchValue, setSearchValue] = useState(''); - const [selectedIds, setSelectedIds] = useState([]); // Rename modal state const [renamingPolicyId, setRenamingPolicyId] = useState(null); - const [renameOpened, { open: openRename, close: closeRename }] = useDisclosure(false); + const [renameOpened, { close: closeRename }] = useDisclosure(false); const [renameError, setRenameError] = useState(null); + // Policy editor modal state + const [editingPolicy, setEditingPolicy] = useState(null); + const [editingAssociationId, setEditingAssociationId] = useState(null); + const [editorMode, setEditorMode] = useState('edit'); + const [editorOpened, { open: openEditor, close: closeEditor }] = useDisclosure(false); + // Rename mutation hook const updateAssociation = useUpdatePolicyAssociation(); @@ -32,18 +41,18 @@ export default function PoliciesPage() { navigate(`/${countryId}/policies/create`); }; - const handleSelectionChange = (recordId: string, selected: boolean) => { - setSelectedIds((prev) => - selected ? [...prev, recordId] : prev.filter((id) => id !== recordId) - ); - }; - - const isSelected = (recordId: string) => selectedIds.includes(recordId); - - const handleOpenRename = (userPolicyId: string) => { - setRenamingPolicyId(userPolicyId); - setRenameError(null); // Clear any previous error - openRename(); + const handleOpenEditor = (recordId: string, mode: EditorMode = 'edit') => { + const item = data?.find((p) => p.association.id?.toString() === recordId); + if (item) { + setEditingPolicy({ + id: item.association.policyId.toString(), + label: item.association.label || `Policy #${item.association.policyId}`, + parameters: item.policy?.parameters || [], + }); + setEditingAssociationId(recordId); + setEditorMode(mode); + openEditor(); + } }; const handleCloseRename = () => { @@ -94,11 +103,11 @@ export default function PoliciesPage() { { key: 'actions', header: '', - type: 'menu', - actions: [{ label: 'Rename', action: 'rename' }], + type: 'actions', + actions: [{ action: 'edit', tooltip: 'View/edit policy', icon: }], onAction: (action: string, recordId: string) => { - if (action === 'rename') { - handleOpenRename(recordId); + if (action === 'edit') { + handleOpenEditor(recordId, 'display'); } }, }, @@ -145,9 +154,6 @@ export default function PoliciesPage() { columns={policyColumns} searchValue={searchValue} onSearchChange={setSearchValue} - enableSelection - isSelected={isSelected} - onSelectionChange={handleSelectionChange} /> @@ -160,6 +166,24 @@ export default function PoliciesPage() { ingredientType="policy" submissionError={renameError} /> + + { + closeEditor(); + setEditingPolicy(null); + setEditingAssociationId(null); + }} + onPolicyCreated={() => { + closeEditor(); + setEditingPolicy(null); + setEditingAssociationId(null); + }} + simulationIndex={0} + initialPolicy={editingPolicy ?? undefined} + initialEditorMode={editorMode} + initialAssociationId={editingAssociationId ?? undefined} + /> ); } diff --git a/app/src/pages/Populations.page.tsx b/app/src/pages/Populations.page.tsx index a477760a6..1a156dc84 100644 --- a/app/src/pages/Populations.page.tsx +++ b/app/src/pages/Populations.page.tsx @@ -45,7 +45,6 @@ export default function PopulationsPage() { const navigate = useNavigate(); const [searchValue, setSearchValue] = useState(''); - const [selectedIds, setSelectedIds] = useState([]); // Rename modal state const [renamingId, setRenamingId] = useState(null); @@ -66,14 +65,6 @@ export default function PopulationsPage() { navigate(`/${countryId}/households/create`); }; - const handleSelectionChange = (recordId: string, selected: boolean) => { - setSelectedIds((prev) => - selected ? [...prev, recordId] : prev.filter((id) => id !== recordId) - ); - }; - - const isSelected = (recordId: string) => selectedIds.includes(recordId); - const handleOpenRename = (recordId: string) => { // Determine type by looking up in the original data // Households use their association.id, geographies use geographyId @@ -326,9 +317,6 @@ export default function PopulationsPage() { columns={populationColumns} searchValue={searchValue} onSearchChange={setSearchValue} - enableSelection - isSelected={isSelected} - onSelectionChange={handleSelectionChange} /> diff --git a/app/src/pages/ReportBuilder.page.tsx b/app/src/pages/ReportBuilder.page.tsx new file mode 100644 index 000000000..83b7e77a5 --- /dev/null +++ b/app/src/pages/ReportBuilder.page.tsx @@ -0,0 +1,5421 @@ +/** + * ReportBuilder - A visual, building-block approach to report configuration + * + * Design Direction: Refined utilitarian with distinct color coding. + * - Policy: Secondary (slate) - authoritative, grounded + * - Population: Primary (teal) - brand-focused, people + * - Dynamics: Blue - forward-looking, data-driven + * + * Two view modes: + * - Card view: 50/50 grid with square chips + * - Row view: Stacked horizontal rows + */ +import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { + IconArrowRight, + IconChartLine, + IconCheck, + IconChevronLeft, + IconChevronRight, + IconCircleCheck, + IconCircleDashed, + IconClock, + IconFileDescription, + IconFolder, + IconHome, + IconInfoCircle, + IconLayoutColumns, + IconPencil, + IconPlayerPlay, + IconPlus, + IconRowInsertBottom, + IconScale, + IconSearch, + IconSparkles, + IconTrash, + IconUsers, + IconX, +} from '@tabler/icons-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; +import { + ActionIcon, + Autocomplete, + Box, + Button, + Container, + Divider, + Drawer, + Group, + Loader, + LoadingOverlay, + Modal, + NavLink, + Paper, + Popover, + ScrollArea, + Select, + Skeleton, + Stack, + Tabs, + Text, + TextInput, + Title, + Tooltip, + Transition, + UnstyledButton, +} from '@mantine/core'; +import { PolicyAdapter } from '@/adapters'; +import { HouseholdAdapter } from '@/adapters/HouseholdAdapter'; +import { geographyUsageStore, householdUsageStore } from '@/api/usageTracking'; +import HouseholdBuilderForm from '@/components/household/HouseholdBuilderForm'; +import { UKOutlineIcon, USOutlineIcon } from '@/components/icons/CountryOutlineIcons'; +import { CURRENT_YEAR, MOCK_USER_ID } from '@/constants'; +import { colors, spacing, typography } from '@/designTokens'; +import { useCreateHousehold } from '@/hooks/useCreateHousehold'; +import { useCreatePolicy } from '@/hooks/useCreatePolicy'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useUserHouseholds } from '@/hooks/useUserHousehold'; +import { useUpdatePolicyAssociation, useUserPolicies } from '@/hooks/useUserPolicy'; +import { getBasicInputFields, getDateRange } from '@/libs/metadataUtils'; +import { householdAssociationKeys } from '@/libs/queryKeys'; +import HistoricalValues from '@/pathways/report/components/policyParameterSelector/HistoricalValues'; +import { + ModeSelectorButton, + ValueSetterComponents, + ValueSetterMode, +} from '@/pathways/report/components/valueSetters'; +import { RootState } from '@/store'; +import { Geography } from '@/types/ingredients/Geography'; +import { Household } from '@/types/ingredients/Household'; +import { Policy } from '@/types/ingredients/Policy'; +import { ParameterTreeNode } from '@/types/metadata'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { PolicyStateProps, PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; +import { PolicyCreationPayload } from '@/types/payloads'; +import { getParameterByName, Parameter } from '@/types/subIngredients/parameter'; +import { + ValueInterval, + ValueIntervalCollection, + ValuesList, +} from '@/types/subIngredients/valueInterval'; +import { countPolicyModifications } from '@/utils/countParameterChanges'; +import { formatPeriod } from '@/utils/dateUtils'; +import { generateGeographyLabel } from '@/utils/geographyUtils'; +import { HouseholdBuilder } from '@/utils/HouseholdBuilder'; +import { formatLabelParts, getHierarchicalLabels } from '@/utils/parameterLabels'; +import { initializePolicyState } from '@/utils/pathwayState/initializePolicyState'; +import { initializePopulationState } from '@/utils/pathwayState/initializePopulationState'; +import { initializeSimulationState } from '@/utils/pathwayState/initializeSimulationState'; +import { formatParameterValue } from '@/utils/policyTableHelpers'; +import { + getUKConstituencies, + getUKCountries, + getUKLocalAuthorities, + getUSCongressionalDistricts, + getUSStates, + RegionOption, +} from '@/utils/regionStrategies'; +import { capitalize } from '@/utils/stringUtils'; +import { PolicyCreationModal } from './reportBuilder/modals'; + +// ============================================================================ +// TYPES +// ============================================================================ + +interface ReportBuilderState { + label: string | null; + year: string; + simulations: SimulationStateProps[]; +} + +type IngredientType = 'policy' | 'population' | 'dynamics'; +type ViewMode = 'cards' | 'rows'; + +interface IngredientPickerState { + isOpen: boolean; + simulationIndex: number; + ingredientType: IngredientType; +} + +// ============================================================================ +// DESIGN TOKENS +// ============================================================================ + +const FONT_SIZES = { + title: '28px', + normal: '14px', + small: '12px', + tiny: '10px', +}; + +// Distinct color palette for each ingredient type +const INGREDIENT_COLORS = { + policy: { + icon: colors.secondary[600], + bg: colors.secondary[50], + border: colors.secondary[200], + accent: colors.secondary[500], + }, + population: { + icon: colors.primary[600], + bg: colors.primary[50], + border: colors.primary[200], + accent: colors.primary[500], + }, + dynamics: { + // Muted gray-green for dynamics (distinct from teal and slate) + icon: colors.gray[500], + bg: colors.gray[50], + border: colors.gray[200], + accent: colors.gray[400], + }, +}; + +// Country-specific configuration +const COUNTRY_CONFIG = { + us: { + nationwideTitle: 'United States', + nationwideSubtitle: 'Nationwide', + nationwideLabel: 'United States', // Used for geography name + nationwideId: 'us-nationwide', + geographyId: 'us', + }, + uk: { + nationwideTitle: 'United Kingdom', + nationwideSubtitle: 'UK-wide', + nationwideLabel: 'United Kingdom', // Used for geography name + nationwideId: 'uk-nationwide', + geographyId: 'uk', + }, +} as const; + +// Helper to get sample populations for a country +const getSamplePopulations = (countryId: 'us' | 'uk') => { + const config = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + return { + household: { + label: 'Smith family (4 members)', + type: 'household' as const, + household: { + id: 'sample-household', + countryId, + householdData: { people: { person1: { age: { 2025: 40 } } } }, + }, + geography: null, + }, + nationwide: { + label: config.nationwideLabel, + type: 'geography' as const, + household: null, + geography: { + id: config.nationwideId, + countryId, + scope: 'national' as const, + geographyId: config.geographyId, + name: config.nationwideLabel, + }, + }, + }; +}; + +// Country-specific map icon component +function CountryMapIcon({ + countryId, + size, + color, +}: { + countryId: string; + size: number; + color: string; +}) { + if (countryId === 'uk') { + return ; + } + return ; +} + +// Default sample populations (for backwards compatibility) +const SAMPLE_POPULATIONS = getSamplePopulations('us'); + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = { + pageContainer: { + minHeight: '100vh', + background: `linear-gradient(180deg, ${colors.gray[50]} 0%, ${colors.background.secondary} 100%)`, + padding: `${spacing.lg} ${spacing['3xl']}`, + }, + + headerSection: { + marginBottom: spacing.xl, + }, + + mainTitle: { + fontFamily: typography.fontFamily.primary, + fontSize: FONT_SIZES.title, + fontWeight: typography.fontWeight.bold, + color: colors.gray[900], + letterSpacing: '-0.02em', + margin: 0, + }, + + canvasContainer: { + background: colors.white, + borderRadius: spacing.radius.xl, + border: `1px solid ${colors.border.light}`, + boxShadow: `0 4px 24px ${colors.shadow.light}`, + padding: spacing['2xl'], + position: 'relative' as const, + overflow: 'hidden', + }, + + canvasGrid: { + background: ` + linear-gradient(90deg, ${colors.gray[100]}18 1px, transparent 1px), + linear-gradient(${colors.gray[100]}18 1px, transparent 1px) + `, + backgroundSize: '20px 20px', + position: 'absolute' as const, + inset: 0, + pointerEvents: 'none' as const, + }, + + simulationsGrid: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: 'auto auto auto auto', // header, policy, population, dynamics + gap: `${spacing.sm} ${spacing['2xl']}`, + position: 'relative' as const, + zIndex: 1, + minHeight: '450px', + alignItems: 'start', + }, + + simulationCard: { + background: colors.white, + borderRadius: spacing.radius.lg, + border: `2px solid ${colors.gray[200]}`, + padding: spacing.xl, + transition: 'all 0.2s ease', + position: 'relative' as const, + display: 'grid', + gridRow: 'span 4', // span all 4 rows (header + 3 panels) + gridTemplateRows: 'subgrid', + gap: spacing.sm, + }, + + simulationCardActive: { + border: `2px solid ${colors.primary[400]}`, + boxShadow: `0 0 0 4px ${colors.primary[50]}, 0 8px 32px ${colors.shadow.medium}`, + }, + + simulationHeader: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.lg, + }, + + simulationTitle: { + fontFamily: typography.fontFamily.primary, + fontSize: FONT_SIZES.normal, + fontWeight: typography.fontWeight.semibold, + color: colors.gray[800], + }, + + // Ingredient section (bubble/card container, not clickable) + ingredientSection: { + padding: spacing.md, + borderRadius: spacing.radius.lg, + border: `1px solid`, + background: 'white', + }, + + ingredientSectionHeader: { + display: 'flex', + alignItems: 'center', + gap: spacing.sm, + marginBottom: spacing.md, + }, + + ingredientSectionIcon: { + width: 32, + height: 32, + borderRadius: spacing.radius.md, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + + // Chip grid for card view (square chips, 3 per row) + chipGridSquare: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: spacing.sm, + }, + + // Row layout for row view + chipRowContainer: { + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.xs, + }, + + // Square chip (expands to fill grid cell, min 80px height) + chipSquare: { + minHeight: 80, + borderRadius: spacing.radius.md, + borderWidth: 1, + borderStyle: 'solid', + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: 6, + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease', + padding: spacing.sm, + }, + + chipSquareSelected: { + borderWidth: 2, + boxShadow: `0 0 0 2px`, + }, + + // Row chip (80 height) + chipRow: { + display: 'flex', + alignItems: 'center', + gap: spacing.md, + padding: `${spacing.md} ${spacing.lg}`, + borderRadius: spacing.radius.md, + borderWidth: 1, + borderStyle: 'solid', + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease', + minHeight: 80, + }, + + chipRowSelected: { + borderWidth: 2, + }, + + chipRowIcon: { + width: 40, + height: 40, + borderRadius: spacing.radius.md, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }, + + // Perforated "Create new policy" chip (expands to fill grid cell) + chipCustomSquare: { + minHeight: 80, + borderRadius: spacing.radius.md, + borderWidth: 2, + borderStyle: 'dashed', + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: 6, + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease', + padding: spacing.sm, + }, + + chipCustomRow: { + display: 'flex', + alignItems: 'center', + gap: spacing.md, + padding: `${spacing.md} ${spacing.lg}`, + borderRadius: spacing.radius.md, + borderWidth: 2, + borderStyle: 'dashed', + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease', + minHeight: 80, + }, + + addSimulationCard: { + background: colors.white, + borderRadius: spacing.radius.lg, + border: `2px dashed ${colors.border.medium}`, + padding: spacing.xl, + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: spacing.md, + cursor: 'pointer', + transition: 'all 0.2s ease', + gridRow: 'span 4', // span all 4 rows to match SimulationBlock + }, + + reportMetaCard: { + background: colors.white, + borderRadius: spacing.radius.lg, + border: `1px solid ${colors.border.light}`, + padding: `${spacing.xl} ${spacing.xl} ${spacing['2xl']} ${spacing.xl}`, + marginBottom: spacing.xl, + position: 'relative' as const, + overflow: 'hidden', + }, + + inheritedBadge: { + fontSize: FONT_SIZES.tiny, + color: colors.gray[500], + fontStyle: 'italic', + marginLeft: spacing.xs, + }, +}; + +// ============================================================================ +// SUB-COMPONENTS +// ============================================================================ + +// Color config type that accepts any ingredient color variant +type IngredientColorConfig = { + icon: string; + bg: string; + border: string; + accent: string; +}; + +interface OptionChipSquareProps { + icon: React.ReactNode; + label: string; + description?: string; + isSelected: boolean; + onClick: () => void; + colorConfig: IngredientColorConfig; +} + +function OptionChipSquare({ + icon, + label, + description, + isSelected, + onClick, + colorConfig, +}: OptionChipSquareProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + {icon} + + + {label} + + {description && ( + + {description} + + )} + + ); +} + +interface OptionChipRowProps { + icon: React.ReactNode; + label: string; + description?: string; + isSelected: boolean; + onClick: () => void; + colorConfig: IngredientColorConfig; +} + +function OptionChipRow({ + icon, + label, + description, + isSelected, + onClick, + colorConfig, +}: OptionChipRowProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + {icon} + + + + {label} + + {description && ( + + {description} + + )} + + {isSelected && } + + ); +} + +interface CreateCustomChipProps { + label: string; + onClick: () => void; + variant: 'square' | 'row'; + colorConfig: IngredientColorConfig; +} + +function CreateCustomChip({ label, onClick, variant, colorConfig }: CreateCustomChipProps) { + const [isHovered, setIsHovered] = useState(false); + + if (variant === 'square') { + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + {label} + + + ); + } + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + + + {label} + + + ); +} + +interface SavedPolicy { + id: string; + label: string; + paramCount: number; + createdAt?: string; + updatedAt?: string; +} + +interface BrowseMoreChipProps { + label: string; + description?: string; + onClick: () => void; + variant: 'square' | 'row'; + colorConfig: IngredientColorConfig; +} + +function BrowseMoreChip({ + label, + description, + onClick, + variant, + colorConfig, +}: BrowseMoreChipProps) { + const [isHovered, setIsHovered] = useState(false); + + if (variant === 'square') { + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + {label} + + {description && ( + + {description} + + )} + + ); + } + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + + + + {label} + + {description && ( + + {description} + + )} + + + ); +} + +// Recent population for display in IngredientSection +interface RecentPopulation { + id: string; + label: string; + type: 'geography' | 'household'; + population: PopulationStateProps; +} + +interface IngredientSectionProps { + type: IngredientType; + currentId?: string; + countryId?: 'us' | 'uk'; + onQuickSelectPolicy?: (type: 'current-law') => void; + onSelectSavedPolicy?: (id: string, label: string, paramCount: number) => void; + onQuickSelectPopulation?: (type: 'nationwide') => void; + onSelectRecentPopulation?: (population: PopulationStateProps) => void; + onDeselectPopulation?: () => void; + onDeselectPolicy?: () => void; + onCreateCustom: () => void; + onBrowseMore?: () => void; + isInherited?: boolean; + inheritedPopulationType?: 'household' | 'nationwide' | null; + savedPolicies?: SavedPolicy[]; + recentPopulations?: RecentPopulation[]; + viewMode: ViewMode; +} + +function IngredientSection({ + type, + currentId, + countryId = 'us', + onQuickSelectPolicy, + onSelectSavedPolicy, + onQuickSelectPopulation, + onSelectRecentPopulation, + onDeselectPopulation, + onDeselectPolicy, + onCreateCustom, + onBrowseMore, + isInherited, + inheritedPopulationType, + savedPolicies = [], + recentPopulations = [], + viewMode, +}: IngredientSectionProps) { + const countryConfig = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + const colorConfig = INGREDIENT_COLORS[type]; + const IconComponent = { + policy: IconScale, + population: IconUsers, + dynamics: IconChartLine, + }[type]; + + const typeLabels = { + policy: 'Policy', + population: 'Household(s)', + dynamics: 'Dynamics', + }; + + const useRowLayout = viewMode === 'rows'; + const chipVariant = useRowLayout ? 'row' : 'square'; + const iconSize = useRowLayout ? 20 : 16; + + const ChipComponent = useRowLayout ? OptionChipRow : OptionChipSquare; + + return ( + + {/* Section header */} + + + + + + {typeLabels[type]} + + {isInherited && (inherited from baseline)} + + + {/* Chips container */} + {isInherited && inheritedPopulationType ? ( + + + {useRowLayout ? ( + <> + + {inheritedPopulationType === 'household' ? ( + + ) : ( + + )} + + + + {inheritedPopulationType === 'household' + ? 'Household' + : countryConfig.nationwideTitle} + + + {inheritedPopulationType === 'household' + ? 'Inherited from baseline' + : countryConfig.nationwideSubtitle} + + + + ) : ( + <> + + {inheritedPopulationType === 'household' ? ( + + ) : ( + + )} + + + {inheritedPopulationType === 'household' + ? 'Household' + : countryConfig.nationwideTitle} + + + {inheritedPopulationType === 'household' + ? 'Inherited' + : countryConfig.nationwideSubtitle} + + + )} + + + ) : ( + + {type === 'policy' && onQuickSelectPolicy && ( + <> + {/* Current law - always first */} + + } + label="Current law" + description="No changes" + isSelected={currentId === 'current-law'} + onClick={() => { + if (currentId === 'current-law' && onDeselectPolicy) { + onDeselectPolicy(); + } else { + onQuickSelectPolicy('current-law'); + } + }} + colorConfig={colorConfig} + /> + {/* Saved policies - up to 3 shown (total 4 with Current law) */} + {savedPolicies.slice(0, 3).map((policy) => ( + + } + label={policy.label} + description={`${policy.paramCount} param${policy.paramCount !== 1 ? 's' : ''} changed`} + isSelected={currentId === policy.id} + onClick={() => { + if (currentId === policy.id && onDeselectPolicy) { + onDeselectPolicy(); + } else { + onSelectSavedPolicy?.(policy.id, policy.label, policy.paramCount); + } + }} + colorConfig={colorConfig} + /> + ))} + {/* More options - always shown for searching/browsing all policies */} + {onBrowseMore && ( + + )} + + )} + + {type === 'population' && onQuickSelectPopulation && ( + <> + {/* Nationwide - always first */} + + } + label={countryConfig.nationwideTitle} + description={countryConfig.nationwideSubtitle} + isSelected={currentId === countryConfig.nationwideId} + onClick={() => { + if (currentId === countryConfig.nationwideId && onDeselectPopulation) { + onDeselectPopulation(); + } else { + onQuickSelectPopulation('nationwide'); + } + }} + colorConfig={colorConfig} + /> + {/* Recent populations - up to 4 shown */} + {recentPopulations.slice(0, 4).map((pop) => ( + + ) : ( + + ) + } + label={pop.label} + description={pop.type === 'household' ? 'Household' : 'Geography'} + isSelected={currentId === pop.id} + onClick={() => { + if (currentId === pop.id && onDeselectPopulation) { + onDeselectPopulation(); + } else { + onSelectRecentPopulation?.(pop.population); + } + }} + colorConfig={colorConfig} + /> + ))} + {/* More options - always shown for searching/browsing all populations */} + {onBrowseMore && ( + + )} + + )} + + {type === 'dynamics' && ( + + + + + Dynamics coming soon + + + + )} + + )} + + ); +} + +interface SimulationBlockProps { + simulation: SimulationStateProps; + index: number; + countryId: 'us' | 'uk'; + onLabelChange: (label: string) => void; + onQuickSelectPolicy: (policyType: 'current-law') => void; + onSelectSavedPolicy: (id: string, label: string, paramCount: number) => void; + onQuickSelectPopulation: (populationType: 'nationwide') => void; + onSelectRecentPopulation: (population: PopulationStateProps) => void; + onDeselectPolicy: () => void; + onDeselectPopulation: () => void; + onCreateCustomPolicy: () => void; + onBrowseMorePolicies: () => void; + onBrowseMorePopulations: () => void; + onRemove?: () => void; + canRemove: boolean; + isRequired?: boolean; + populationInherited?: boolean; + inheritedPopulation?: PopulationStateProps | null; + savedPolicies: SavedPolicy[]; + recentPopulations: RecentPopulation[]; + viewMode: ViewMode; +} + +function SimulationBlock({ + simulation, + index, + countryId, + onLabelChange, + onQuickSelectPolicy, + onSelectSavedPolicy, + onQuickSelectPopulation, + onSelectRecentPopulation, + onDeselectPolicy, + onDeselectPopulation, + onCreateCustomPolicy, + onBrowseMorePolicies, + onBrowseMorePopulations, + onRemove, + canRemove, + isRequired, + populationInherited, + inheritedPopulation, + savedPolicies, + recentPopulations, + viewMode, +}: SimulationBlockProps) { + const [isEditingLabel, setIsEditingLabel] = useState(false); + const [labelInput, setLabelInput] = useState(simulation.label || ''); + + const isPolicyConfigured = !!simulation.policy.id; + const effectivePopulation = + populationInherited && inheritedPopulation ? inheritedPopulation : simulation.population; + const isPopulationConfigured = !!( + effectivePopulation?.household?.id || effectivePopulation?.geography?.id + ); + const isFullyConfigured = isPolicyConfigured && isPopulationConfigured; + + const handleLabelSubmit = () => { + onLabelChange(labelInput || (index === 0 ? 'Baseline simulation' : 'Reform simulation')); + setIsEditingLabel(false); + }; + + const defaultLabel = index === 0 ? 'Baseline simulation' : 'Reform simulation'; + + const currentPolicyId = simulation.policy.id; + const currentPopulationId = + effectivePopulation?.household?.id || effectivePopulation?.geography?.id; + + // Determine inherited population type for display + const inheritedPopulationType = + populationInherited && inheritedPopulation + ? inheritedPopulation.household?.id + ? 'household' + : inheritedPopulation.geography?.id + ? 'nationwide' + : null + : null; + + return ( + + {/* Status indicator */} + + + {/* Header */} + + + {isEditingLabel ? ( + setLabelInput(e.target.value)} + onBlur={handleLabelSubmit} + onKeyDown={(e) => e.key === 'Enter' && handleLabelSubmit()} + size="sm" + autoFocus + styles={{ + input: { + fontWeight: typography.fontWeight.semibold, + fontSize: FONT_SIZES.normal, + }, + }} + /> + ) : ( + + {simulation.label || defaultLabel} + { + setLabelInput(simulation.label || defaultLabel); + setIsEditingLabel(true); + }} + > + + + + )} + + + + {isRequired && ( + + Required + + )} + {isFullyConfigured && ( + + + + + + )} + {canRemove && ( + + + + )} + + + + {/* Panels - direct children for subgrid alignment */} + + + {}} // Not used for population + onBrowseMore={onBrowseMorePopulations} + isInherited={populationInherited} + inheritedPopulationType={inheritedPopulationType} + recentPopulations={recentPopulations} + viewMode={viewMode} + /> + + {}} + viewMode={viewMode} + /> + + ); +} + +interface AddSimulationCardProps { + onClick: () => void; + disabled?: boolean; +} + +function AddSimulationCard({ onClick, disabled }: AddSimulationCardProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={disabled ? undefined : onClick} + > + + + + + Add reform simulation + + + Compare policy changes against your baseline + + + ); +} + +// ============================================================================ +// INGREDIENT PICKER MODAL +// ============================================================================ + +interface IngredientPickerModalProps { + isOpen: boolean; + onClose: () => void; + type: IngredientType; + onSelect: (item: PolicyStateProps | PopulationStateProps | null) => void; + onCreateNew: () => void; +} + +function IngredientPickerModal({ + isOpen, + onClose, + type, + onSelect, + onCreateNew, +}: IngredientPickerModalProps) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const countryConfig = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + const userId = MOCK_USER_ID.toString(); + const { data: policies, isLoading: policiesLoading } = useUserPolicies(userId); + const { data: households, isLoading: householdsLoading } = useUserHouseholds(userId); + const colorConfig = INGREDIENT_COLORS[type]; + const [expandedPolicyId, setExpandedPolicyId] = useState(null); + const parameters = useSelector((state: RootState) => state.metadata.parameters); + + const getTitle = () => { + switch (type) { + case 'policy': + return 'Select policy'; + case 'population': + return 'Select population'; + case 'dynamics': + return 'Configure dynamics'; + } + }; + + const getIcon = () => { + const iconProps = { size: 20, color: colorConfig.icon }; + switch (type) { + case 'policy': + return ; + case 'population': + return ; + case 'dynamics': + return ; + } + }; + + const handleSelectPolicy = (policyId: string, label: string, paramCount: number) => { + onSelect({ id: policyId, label, parameters: Array(paramCount).fill({}) }); + onClose(); + }; + + const handleSelectCurrentLaw = () => { + onSelect({ id: 'current-law', label: 'Current law', parameters: [] }); + onClose(); + }; + + const handleSelectHousehold = (householdId: string, label: string) => { + onSelect({ + label, + type: 'household', + household: { id: householdId, countryId, householdData: { people: {} } }, + geography: null, + }); + onClose(); + }; + + const handleSelectGeography = ( + geoId: string, + label: string, + scope: 'national' | 'subnational' + ) => { + onSelect({ + label, + type: 'geography', + household: null, + geography: { id: geoId, countryId, scope, geographyId: geoId, name: label }, + }); + onClose(); + }; + + return ( + + + {getIcon()} + + + {getTitle()} + +
+ } + size="xl" + radius="lg" + styles={{ + content: { width: '80vw', maxWidth: '1200px' }, + header: { borderBottom: `1px solid ${colors.border.light}`, paddingBottom: spacing.md }, + body: { padding: spacing.xl }, + }} + > + + {type === 'policy' && ( + <> + + + + + + + + Current law + + + Use existing tax and benefit rules without modifications + + + + + + + {policiesLoading ? ( + + + + ) : ( + + {policies?.map((p) => { + // Use association data for display (like Policies page) + const policyId = p.association.policyId.toString(); + const label = p.association.label || `Policy #${policyId}`; + const paramCount = countPolicyModifications(p.policy); // Handles undefined gracefully + const policyParams = p.policy?.parameters || []; + const isExpanded = expandedPolicyId === policyId; + + return ( + + {/* Main clickable row */} + handleSelectPolicy(policyId, label, paramCount)} + onMouseEnter={(e) => { + e.currentTarget.style.background = colors.gray[50]; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + }} + > + {/* Policy info - takes remaining space */} + + + {label} + + + {paramCount} param{paramCount !== 1 ? 's' : ''} changed + + + + {/* Info/expand button - isolated click zone */} + { + e.stopPropagation(); // Prevent selection + setExpandedPolicyId(isExpanded ? null : policyId); + }} + style={{ marginRight: spacing.sm }} + aria-label={ + isExpanded ? 'Hide parameter details' : 'Show parameter details' + } + > + + + + {/* Select indicator */} + + + + {/* Expandable parameter details - table-like display */} + + {/* Unified grid for header and data rows */} + + {/* Header row */} + + Parameter + + + Changes + + + {/* Data rows - grouped by parameter */} + {(() => { + // Build grouped list of parameters with their changes + const groupedParams: Array<{ + paramName: string; + label: string; + changes: Array<{ period: string; value: string }>; + }> = []; + + policyParams.forEach((param) => { + const paramName = param.name; + const hierarchicalLabels = getHierarchicalLabels( + paramName, + parameters + ); + const displayLabel = + hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : paramName.split('.').pop() || paramName; + const metadata = parameters[paramName]; + + // Use value intervals directly from the Policy type + const changes = (param.values || []).map((interval) => ({ + period: formatPeriod(interval.startDate, interval.endDate), + value: formatParameterValue(interval.value, metadata?.unit), + })); + + groupedParams.push({ paramName, label: displayLabel, changes }); + }); + + if (groupedParams.length === 0) { + return ( + <> + + No parameter details available + + + ); + } + + const displayParams = groupedParams.slice(0, 10); + const remainingCount = groupedParams.length - 10; + + return ( + <> + {displayParams.map((param) => ( + + {/* Parameter name cell */} + + + + {param.label} + + + + {/* Changes cell - multiple lines */} + + {param.changes.map((change, idx) => ( + + + {change.period}: + {' '} + + {change.value} + + + ))} + + + ))} + {remainingCount > 0 && ( + + +{remainingCount} more parameter + {remainingCount !== 1 ? 's' : ''} + + )} + + ); + })()} + + + + ); + })} + {(!policies || policies.length === 0) && ( + + No saved policies + + )} + + )} + + + + + )} + + {type === 'population' && ( + <> + + handleSelectGeography( + countryConfig.nationwideId, + countryConfig.nationwideLabel, + 'national' + ) + } + > + + + + + + + {countryConfig.nationwideTitle} + + + {countryConfig.nationwideSubtitle} + + + + + handleSelectHousehold('sample-household', 'Sample household')} + > + + + + + + + Sample household + + + Single household simulation + + + + + + + {householdsLoading ? ( + + + + ) : ( + + {households?.map((h) => { + // Use association data for display (like Populations page) + const householdId = h.association.householdId.toString(); + const label = h.association.label || `Household #${householdId}`; + return ( + handleSelectHousehold(householdId, label)} + > + + + {label} + + + + + ); + })} + {(!households || households.length === 0) && ( + + No saved households + + )} + + )} + + + + + )} + + {type === 'dynamics' && ( + + + + + + + Dynamics coming soon + + + Dynamic behavioral responses will be available in a future update. + + + + )} + + + ); +} + +// ============================================================================ +// POLICY BROWSE MODAL - Augmented policy selection experience with integrated creation +// ============================================================================ + +interface PolicyBrowseModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (policy: PolicyStateProps) => void; +} + +function PolicyBrowseModal({ isOpen, onClose, onSelect }: PolicyBrowseModalProps) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const userId = MOCK_USER_ID.toString(); + const { data: policies, isLoading } = useUserPolicies(userId); + const { + parameterTree, + parameters, + loading: metadataLoading, + } = useSelector((state: RootState) => state.metadata); + const { minDate, maxDate } = useSelector(getDateRange); + const updatePolicyAssociation = useUpdatePolicyAssociation(); + + // Browse mode state + const [searchQuery, setSearchQuery] = useState(''); + const [activeSection, setActiveSection] = useState<'my-policies' | 'public'>('my-policies'); + const [selectedPolicyId, setSelectedPolicyId] = useState(null); + const [drawerPolicyId, setDrawerPolicyId] = useState(null); + + // Creation mode state + const [isCreationMode, setIsCreationMode] = useState(false); + const [policyLabel, setPolicyLabel] = useState(''); + const [policyParameters, setPolicyParameters] = useState([]); + const [selectedParam, setSelectedParam] = useState(null); + const [expandedMenuItems, setExpandedMenuItems] = useState>(new Set()); + const [valueSetterMode, setValueSetterMode] = useState(ValueSetterMode.DEFAULT); + const [intervals, setIntervals] = useState([]); + const [startDate, setStartDate] = useState('2025-01-01'); + const [endDate, setEndDate] = useState('2025-12-31'); + const [parameterSearch, setParameterSearch] = useState(''); + const [isEditingLabel, setIsEditingLabel] = useState(false); + + // API hook for creating policy + const { createPolicy, isPending: isCreating } = useCreatePolicy(policyLabel || undefined); + + // Reset state on mount + useEffect(() => { + if (isOpen) { + setSearchQuery(''); + setSelectedPolicyId(null); + setDrawerPolicyId(null); + setIsCreationMode(false); + setPolicyLabel(''); + setPolicyParameters([]); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + setIsEditingLabel(false); + } + }, [isOpen]); + + // Transform policies data, sorted by most recent + // Uses association data for display (like Policies page), policy data only for param count + const userPolicies = useMemo(() => { + return (policies || []) + .map((p) => { + const policyId = p.association.policyId.toString(); + const label = p.association.label || `Policy #${policyId}`; + return { + id: policyId, + associationId: p.association.id, // For updating updatedAt on selection + label, + paramCount: countPolicyModifications(p.policy), // Handles undefined gracefully + parameters: p.policy?.parameters || [], + createdAt: p.association.createdAt, + updatedAt: p.association.updatedAt, + }; + }) + .sort((a, b) => { + // Sort by most recent timestamp (prefer updatedAt, fallback to createdAt) + const aTime = a.updatedAt || a.createdAt || ''; + const bTime = b.updatedAt || b.createdAt || ''; + return bTime.localeCompare(aTime); // Descending (most recent first) + }); + }, [policies]); + + // Filter policies based on search + const filteredPolicies = useMemo(() => { + let result = userPolicies; + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + result = result.filter((p) => { + // Search in policy label + if (p.label.toLowerCase().includes(query)) return true; + // Search in parameter display names (hierarchical labels) + const paramDisplayNames = p.parameters + .map((param) => { + const hierarchicalLabels = getHierarchicalLabels(param.name, parameters); + return hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : param.name.split('.').pop() || param.name; + }) + .join(' ') + .toLowerCase(); + if (paramDisplayNames.includes(query)) return true; + return false; + }); + } + + return result; + }, [userPolicies, searchQuery, parameters]); + + // Get policies for current section + const displayedPolicies = useMemo(() => { + if (activeSection === 'public') { + // TODO: Fetch public policies from API when available + return []; + } + // 'my-policies' - already sorted by most recent + return filteredPolicies; + }, [activeSection, filteredPolicies]); + + // Get section title for display + const getSectionTitle = () => { + switch (activeSection) { + case 'my-policies': + return 'My policies'; + case 'public': + return 'User-created policies'; + default: + return 'Policies'; + } + }; + + // Handle policy selection + const handleSelectPolicy = ( + policyId: string, + label: string, + paramCount: number, + associationId?: string + ) => { + // Update the association's updatedAt to track "recently used" + if (associationId) { + updatePolicyAssociation.mutate({ + userPolicyId: associationId, + updates: {}, // updatedAt is set automatically by the store + }); + } + // Call onSelect with policy state + onSelect({ id: policyId, label, parameters: Array(paramCount).fill({}) }); + onClose(); + }; + + // Handle current law selection + const handleSelectCurrentLaw = () => { + onSelect({ id: 'current-law', label: 'Current law', parameters: [] }); + onClose(); + }; + + // ========== Creation Mode Logic ========== + + // Create local policy state object for components + const localPolicy: PolicyStateProps = useMemo( + () => ({ + label: policyLabel, + parameters: policyParameters, + }), + [policyLabel, policyParameters] + ); + + // Count modifications + const modificationCount = countPolicyModifications(localPolicy); + + // Build flat list of all searchable parameters for autocomplete + const searchableParameters = useMemo(() => { + if (!parameters) return []; + + return Object.values(parameters) + .filter( + (param): param is ParameterMetadata => + param.type === 'parameter' && !!param.label && !param.parameter.includes('pycache') + ) + .map((param) => { + const hierarchicalLabels = getHierarchicalLabels(param.parameter, parameters); + const fullLabel = + hierarchicalLabels.length > 0 ? formatLabelParts(hierarchicalLabels) : param.label; + return { + value: param.parameter, + label: fullLabel, + }; + }) + .sort((a, b) => a.label.localeCompare(b.label)); + }, [parameters]); + + // Handle search selection - expand tree path and select parameter + const handleSearchSelect = useCallback( + (paramName: string) => { + const param = parameters[paramName]; + if (!param || param.type !== 'parameter') return; + + // Expand all parent nodes in the tree path + const pathParts = paramName.split('.'); + const newExpanded = new Set(expandedMenuItems); + let currentPath = ''; + for (let i = 0; i < pathParts.length - 1; i++) { + currentPath = currentPath ? `${currentPath}.${pathParts[i]}` : pathParts[i]; + newExpanded.add(currentPath); + } + setExpandedMenuItems(newExpanded); + + // Select the parameter + setSelectedParam(param); + setIntervals([]); + setValueSetterMode(ValueSetterMode.DEFAULT); + + // Clear search + setParameterSearch(''); + }, + [parameters, expandedMenuItems] + ); + + // Handle menu item click + const handleMenuItemClick = useCallback( + (paramName: string) => { + const param = parameters[paramName]; + if (param && param.type === 'parameter') { + setSelectedParam(param); + // Reset value setter state when selecting new parameter + setIntervals([]); + setValueSetterMode(ValueSetterMode.DEFAULT); + } + // Toggle expansion for non-leaf nodes + setExpandedMenuItems((prev) => { + const newSet = new Set(prev); + if (newSet.has(paramName)) { + newSet.delete(paramName); + } else { + newSet.add(paramName); + } + return newSet; + }); + }, + [parameters] + ); + + // Handle value submission + const handleValueSubmit = useCallback(() => { + if (!selectedParam || intervals.length === 0) return; + + const updatedParameters = [...policyParameters]; + let existingParam = updatedParameters.find((p) => p.name === selectedParam.parameter); + + if (!existingParam) { + existingParam = { name: selectedParam.parameter, values: [] }; + updatedParameters.push(existingParam); + } + + // Use ValueIntervalCollection to properly merge intervals + const paramCollection = new ValueIntervalCollection(existingParam.values); + intervals.forEach((interval) => { + paramCollection.addInterval(interval); + }); + + existingParam.values = paramCollection.getIntervals(); + setPolicyParameters(updatedParameters); + setIntervals([]); + }, [selectedParam, intervals, policyParameters]); + + // Handle entering creation mode + const handleEnterCreationMode = useCallback(() => { + setPolicyLabel(''); + setPolicyParameters([]); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + setIsEditingLabel(false); + setIsCreationMode(true); + }, []); + + // Exit creation mode (back to browse) + const handleExitCreationMode = useCallback(() => { + setIsCreationMode(false); + setPolicyLabel(''); + setPolicyParameters([]); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + }, []); + + // Handle policy creation + const handleCreatePolicy = useCallback(async () => { + if (!policyLabel.trim()) { + return; + } + + const policyData: Partial = { + parameters: policyParameters, + }; + + const payload: PolicyCreationPayload = PolicyAdapter.toCreationPayload(policyData as Policy); + + try { + const result = await createPolicy(payload); + const createdPolicy: PolicyStateProps = { + id: result.result.policy_id, + label: policyLabel, + parameters: policyParameters, + }; + onSelect(createdPolicy); + onClose(); + } catch (error) { + console.error('Failed to create policy:', error); + } + }, [policyLabel, policyParameters, createPolicy, onSelect, onClose]); + + // Render nested menu recursively + const renderMenuItems = useCallback( + (items: ParameterTreeNode[]): React.ReactNode => { + return items + .filter((item) => !item.name.includes('pycache')) + .map((item) => ( + handleMenuItemClick(item.name)} + childrenOffset={16} + style={{ + borderRadius: spacing.radius.sm, + }} + > + {item.children && expandedMenuItems.has(item.name) && renderMenuItems(item.children)} + + )); + }, + [selectedParam?.parameter, expandedMenuItems, handleMenuItemClick] + ); + + // Memoize the rendered tree + const renderedMenuTree = useMemo(() => { + if (metadataLoading || !parameterTree) return null; + return renderMenuItems(parameterTree.children || []); + }, [metadataLoading, parameterTree, renderMenuItems]); + + // Get base and reform values for chart + const getChartValues = () => { + if (!selectedParam) return { baseValues: null, reformValues: null }; + + const baseValues = new ValueIntervalCollection(selectedParam.values as ValuesList); + const reformValues = new ValueIntervalCollection(baseValues); + + const paramToChart = policyParameters.find((p) => p.name === selectedParam.parameter); + if (paramToChart && paramToChart.values && paramToChart.values.length > 0) { + const userIntervals = new ValueIntervalCollection(paramToChart.values as ValuesList); + for (const interval of userIntervals.getIntervals()) { + reformValues.addInterval(interval); + } + } + + return { baseValues, reformValues }; + }; + + const { baseValues, reformValues } = getChartValues(); + const ValueSetterToRender = ValueSetterComponents[valueSetterMode]; + + const colorConfig = INGREDIENT_COLORS.policy; + + // Styles for the modal + const modalStyles = { + sidebar: { + width: 220, + borderRight: `1px solid ${colors.border.light}`, + paddingRight: spacing.lg, + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.lg, + }, + sidebarSection: { + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.xs, + }, + sidebarLabel: { + fontSize: FONT_SIZES.small, + fontWeight: typography.fontWeight.semibold, + color: colors.gray[500], + padding: `0 ${spacing.sm}`, + marginBottom: spacing.xs, + }, + sidebarItem: { + display: 'flex', + alignItems: 'center', + gap: spacing.sm, + padding: `${spacing.sm} ${spacing.md}`, + borderRadius: spacing.radius.md, + cursor: 'pointer', + transition: 'all 0.15s ease', + fontSize: FONT_SIZES.small, + fontWeight: typography.fontWeight.medium, + }, + mainContent: { + flex: 1, + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.lg, + minWidth: 0, + }, + searchBar: { + position: 'relative' as const, + }, + policyGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', + gap: spacing.md, + }, + policyCard: { + background: colors.white, + border: `1px solid ${colors.border.light}`, + borderRadius: spacing.radius.lg, + padding: spacing.lg, + cursor: 'pointer', + transition: 'all 0.2s ease', + position: 'relative' as const, + overflow: 'hidden', + }, + policyCardHovered: { + borderColor: colorConfig.border, + boxShadow: `0 4px 12px ${colors.shadow.light}`, + transform: 'translateY(-2px)', + }, + policyCardSelected: { + borderColor: colorConfig.accent, + background: colorConfig.bg, + }, + }; + + // Dock styles for creation mode status header + const dockStyles = { + statusHeader: { + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + borderRadius: spacing.radius.lg, + border: `1px solid ${modificationCount > 0 ? colorConfig.border : colors.border.light}`, + boxShadow: + modificationCount > 0 + ? `0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px ${colorConfig.border}` + : `0 2px 12px ${colors.shadow.light}`, + padding: `${spacing.sm} ${spacing.lg}`, + transition: 'all 0.3s ease', + margin: spacing.md, + marginBottom: 0, + }, + divider: { + width: '1px', + height: '24px', + background: colors.gray[200], + flexShrink: 0, + }, + }; + + // Policy for drawer preview + const drawerPolicy = useMemo(() => { + if (!drawerPolicyId) return null; + return userPolicies.find((p) => p.id === drawerPolicyId) || null; + }, [drawerPolicyId, userPolicies]); + + return ( + + + + + + + {isCreationMode ? 'Create policy' : 'Select policy'} + + + {isCreationMode + ? 'Configure parameters for your new policy' + : 'Choose an existing policy or create a new one'} + + + + } + size="90vw" + radius="lg" + styles={{ + content: { + maxWidth: '1400px', + height: '85vh', + maxHeight: '800px', + display: 'flex', + flexDirection: 'column', + }, + header: { + borderBottom: `1px solid ${colors.border.light}`, + paddingBottom: spacing.md, + paddingLeft: spacing.xl, + paddingRight: spacing.xl, + }, + body: { + padding: 0, + flex: 1, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }, + }} + > + {isCreationMode ? ( + // ========== CREATION MODE ========== + <> + + {/* Left Sidebar - Parameter Tree */} + + {/* Parameter Tree */} + + + + PARAMETERS + + } + styles={{ + input: { + fontSize: FONT_SIZES.small, + height: 32, + minHeight: 32, + }, + dropdown: { + maxHeight: 300, + }, + option: { + fontSize: FONT_SIZES.small, + padding: `${spacing.xs} ${spacing.sm}`, + }, + }} + size="xs" + /> + + + + {metadataLoading || !parameterTree ? ( + + + + + + ) : ( + renderedMenuTree + )} + + + + + + {/* Main Content - Parameter Editor */} + + {/* Status Header Bar */} + + + {/* Left side: Policy icon and editable name */} + + {/* Policy icon */} + + + + + {/* Editable policy name */} + + {isEditingLabel ? ( + setPolicyLabel(e.currentTarget.value)} + onBlur={() => setIsEditingLabel(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') setIsEditingLabel(false); + if (e.key === 'Escape') setIsEditingLabel(false); + }} + autoFocus + placeholder="Enter policy name..." + size="xs" + style={{ width: 250 }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + border: 'none', + background: 'transparent', + padding: 0, + }, + }} + /> + ) : ( + <> + setIsEditingLabel(true)} + > + {policyLabel || 'Click to name your policy...'} + + setIsEditingLabel(true)} + style={{ flexShrink: 0 }} + > + + + + )} + + + + {/* Right side: Modification count */} + + {/* Modification count with status indicator */} + + {modificationCount > 0 ? ( + <> + + + {modificationCount} parameter{modificationCount !== 1 ? 's' : ''}{' '} + modified + + + ) : ( + + No changes yet + + )} + + + + + + {/* Parameter Editor Content */} + + {!selectedParam ? ( + + + + + + + Select a parameter from the menu to modify its value for your policy reform. + + + + ) : ( + + + {/* Parameter Header */} + + + {capitalize(selectedParam.label || 'Label unavailable')} + + {selectedParam.description && ( + + {selectedParam.description} + + )} + + + {/* Value Setter */} + + + + Set new value + + + + + + + { + setIntervals([]); + setValueSetterMode(mode); + }} + /> + + + + + + {/* Historical Values Chart */} + {baseValues && reformValues && ( + + + + )} + + + )} + + + + + {/* Footer for creation mode */} + + + + + + + + ) : ( + // ========== BROWSE MODE ========== + <> + + {/* Left Sidebar */} + + {/* Quick Actions */} + + Quick select + { + e.currentTarget.style.background = colorConfig.bg; + e.currentTarget.style.borderColor = colorConfig.border; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = colors.gray[50]; + e.currentTarget.style.borderColor = colors.border.light; + }} + > + + + Current law + + + + + + + {/* Navigation Sections */} + + Library + + {/* My Policies (sorted by most recent) */} + setActiveSection('my-policies')} + > + + My policies + + {userPolicies.length} + + + + {/* User-created policies */} + setActiveSection('public')} + > + + User-created policies + + + {/* Create New Policy */} + + + Create new policy + + + + + {/* Main Content Area */} + + {/* Search Bar */} + + } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + size="sm" + styles={{ + input: { + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + fontSize: FONT_SIZES.small, + '&:focus': { + borderColor: colorConfig.accent, + }, + }, + }} + /> + + + {/* Section Header */} + + + {getSectionTitle()} + + + {displayedPolicies.length}{' '} + {displayedPolicies.length === 1 ? 'policy' : 'policies'} + + + + {/* Policy Grid */} + + {isLoading ? ( + + {[1, 2, 3].map((i) => ( + + ))} + + ) : activeSection === 'public' ? ( + // Placeholder for User-created policies section + + + + + + Coming soon + + + Search and browse policies created by other PolicyEngine users. + + + ) : displayedPolicies.length === 0 ? ( + + + + + + {searchQuery ? 'No policies match your search' : 'No policies yet'} + + + {searchQuery + ? 'Try adjusting your search terms or browse all policies' + : 'Create your first policy to get started'} + + {!searchQuery && ( + + )} + + ) : ( + + {displayedPolicies.map((policy) => { + const isSelected = selectedPolicyId === policy.id; + + return ( + { + setSelectedPolicyId(policy.id); + handleSelectPolicy( + policy.id, + policy.label, + policy.paramCount, + policy.associationId + ); + }} + > + {/* Policy accent bar */} + + + + + + {policy.label} + + + {policy.paramCount} param{policy.paramCount !== 1 ? 's' : ''}{' '} + changed + + + + + {/* Info button */} + { + e.stopPropagation(); + setDrawerPolicyId(policy.id); + }} + > + + + {/* Select indicator */} + + + + + ); + })} + + )} + + + + + {/* Sliding panel overlay - click to close */} + + {(transitionStyles) => ( + setDrawerPolicyId(null)} + /> + )} + + + {/* Sliding panel for policy details */} + + {(transitionStyles) => ( + e.stopPropagation()} + > + {drawerPolicy && ( + <> + {/* Panel header */} + + + + + {drawerPolicy.label} + + + {drawerPolicy.paramCount} parameter + {drawerPolicy.paramCount !== 1 ? 's' : ''} changed from current law + + + setDrawerPolicyId(null)} + > + + + + + + {/* Panel body */} + + + {/* Unified grid for header and data rows */} + + {/* Header row */} + + Parameter + + + Changes + + + {/* Data rows - grouped by parameter */} + {(() => { + const groupedParams: Array<{ + paramName: string; + label: string; + changes: Array<{ period: string; value: string }>; + }> = []; + + drawerPolicy.parameters.forEach((param) => { + const paramName = param.name; + const hierarchicalLabels = getHierarchicalLabels( + paramName, + parameters + ); + const displayLabel = + hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : paramName.split('.').pop() || paramName; + const metadata = parameters[paramName]; + + // Use value intervals directly from the Policy type + const changes = (param.values || []).map((interval) => ({ + period: formatPeriod(interval.startDate, interval.endDate), + value: formatParameterValue(interval.value, metadata?.unit), + })); + + groupedParams.push({ paramName, label: displayLabel, changes }); + }); + + return groupedParams.map((param) => ( + + {/* Parameter name cell */} + + + + {param.label} + + + + {/* Period column */} + + {param.changes.map((change, idx) => ( + + {change.period} + + ))} + + {/* Value column */} + + {param.changes.map((change, idx) => ( + + {change.value} + + ))} + + + )); + })()} + + + + + {/* Panel footer */} + + + + + )} + + )} + + + )} + + ); +} + +// ============================================================================ +// POPULATION BROWSE MODAL - Geography and household selection +// ============================================================================ + +type PopulationCategory = + | 'national' + | 'states' + | 'districts' + | 'countries' + | 'constituencies' + | 'local-authorities' + | 'my-households'; + +interface PopulationBrowseModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (population: PopulationStateProps) => void; + onCreateNew: () => void; +} + +function PopulationBrowseModal({ + isOpen, + onClose, + onSelect, + onCreateNew, +}: PopulationBrowseModalProps) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const userId = MOCK_USER_ID.toString(); + const queryClient = useQueryClient(); + const { data: households, isLoading: householdsLoading } = useUserHouseholds(userId); + const regionOptions = useSelector((state: RootState) => state.metadata.economyOptions.region); + const metadata = useSelector((state: RootState) => state.metadata); + const basicInputFields = useSelector(getBasicInputFields); + + // State + const [searchQuery, setSearchQuery] = useState(''); + const [activeCategory, setActiveCategory] = useState('national'); + + // Creation mode state + const [isCreationMode, setIsCreationMode] = useState(false); + const [householdLabel, setHouseholdLabel] = useState(''); + const [householdDraft, setHouseholdDraft] = useState(null); + const [isEditingLabel, setIsEditingLabel] = useState(false); + + // Get report year (default to current year) + const reportYear = CURRENT_YEAR.toString(); + + // Create household hook + const { createHousehold, isPending: isCreating } = useCreateHousehold( + householdLabel || undefined + ); + + // Get all basic non-person fields dynamically + const basicNonPersonFields = useMemo(() => { + return Object.entries(basicInputFields) + .filter(([key]) => key !== 'person') + .flatMap(([, fields]) => fields); + }, [basicInputFields]); + + // Derive marital status and number of children from household draft + const householdPeople = useMemo(() => { + if (!householdDraft) return []; + return Object.keys(householdDraft.householdData.people || {}); + }, [householdDraft]); + + const maritalStatus = householdPeople.includes('your partner') ? 'married' : 'single'; + const numChildren = householdPeople.filter((p) => p.includes('dependent')).length; + + // Reset state on mount + useEffect(() => { + if (isOpen) { + setSearchQuery(''); + setActiveCategory('national'); + setIsCreationMode(false); + setHouseholdLabel(''); + setHouseholdDraft(null); + setIsEditingLabel(false); + } + }, [isOpen]); + + // Get geography categories based on country + const geographyCategories = useMemo(() => { + if (countryId === 'uk') { + const ukCountries = getUKCountries(regionOptions); + const ukConstituencies = getUKConstituencies(regionOptions); + const ukLocalAuthorities = getUKLocalAuthorities(regionOptions); + return [ + { + id: 'countries' as const, + label: 'Countries', + count: ukCountries.length, + regions: ukCountries, + }, + { + id: 'constituencies' as const, + label: 'Constituencies', + count: ukConstituencies.length, + regions: ukConstituencies, + }, + { + id: 'local-authorities' as const, + label: 'Local authorities', + count: ukLocalAuthorities.length, + regions: ukLocalAuthorities, + }, + ]; + } + // US + const usStates = getUSStates(regionOptions); + const usDistricts = getUSCongressionalDistricts(regionOptions); + return [ + { id: 'states' as const, label: 'States', count: usStates.length, regions: usStates }, + { + id: 'districts' as const, + label: 'Congressional districts', + count: usDistricts.length, + regions: usDistricts, + }, + ]; + }, [countryId, regionOptions]); + + // Get regions for active category + const activeRegions = useMemo(() => { + const category = geographyCategories.find((c) => c.id === activeCategory); + return category?.regions || []; + }, [activeCategory, geographyCategories]); + + // Transform households with usage tracking sort + const sortedHouseholds = useMemo(() => { + if (!households) return []; + + return [...households] + .map((h) => { + // Ensure householdId is always a string for consistent comparisons + const householdIdStr = String(h.association.householdId); + // Get usage timestamp, fall back to association's updatedAt + const usageTimestamp = householdUsageStore.getLastUsed(householdIdStr); + const sortTimestamp = + usageTimestamp || h.association.updatedAt || h.association.createdAt || ''; + return { + id: householdIdStr, + label: h.association.label || `Household #${householdIdStr}`, + memberCount: h.household?.household_json?.people + ? Object.keys(h.household.household_json.people).length + : 0, + sortTimestamp, + household: h.household, + }; + }) + .sort((a, b) => b.sortTimestamp.localeCompare(a.sortTimestamp)); + }, [households]); + + // Filter regions/households based on search + const filteredRegions = useMemo(() => { + if (!searchQuery.trim()) return activeRegions; + const query = searchQuery.toLowerCase(); + return activeRegions.filter((r) => r.label.toLowerCase().includes(query)); + }, [activeRegions, searchQuery]); + + const filteredHouseholds = useMemo(() => { + if (!searchQuery.trim()) return sortedHouseholds; + const query = searchQuery.toLowerCase(); + return sortedHouseholds.filter((h) => h.label.toLowerCase().includes(query)); + }, [sortedHouseholds, searchQuery]); + + // Handle geography selection + const handleSelectGeography = (region: RegionOption | null) => { + // Create geography object + const geography: Geography = region + ? { + id: `${countryId}-${region.value}`, + countryId, + scope: 'subnational', + geographyId: region.value, + } + : { + id: countryId, + countryId, + scope: 'national', + geographyId: countryId, + }; + + // Record usage + geographyUsageStore.recordUsage(geography.geographyId); + + // Generate label and create population state + const label = generateGeographyLabel(geography); + onSelect({ + geography, + household: null, + label, + type: 'geography', + }); + onClose(); + }; + + // Handle household selection + const handleSelectHousehold = (householdData: (typeof sortedHouseholds)[0]) => { + // Record usage with string ID + const householdIdStr = String(householdData.id); + householdUsageStore.recordUsage(householdIdStr); + + // Convert HouseholdMetadata to Household using the adapter + // If household data isn't available, create a minimal household object with just the ID + let household: Household | null = null; + if (householdData.household) { + household = HouseholdAdapter.fromMetadata(householdData.household); + } else { + // Fallback: create minimal household with ID for selection to work + household = { + id: householdIdStr, + countryId, + householdData: { people: {} }, + }; + } + + const populationState: PopulationStateProps = { + geography: null, + household, + label: householdData.label, + type: 'household', + }; + + onSelect(populationState); + onClose(); + }; + + // Enter creation mode + const handleEnterCreationMode = useCallback(() => { + const builder = new HouseholdBuilder(countryId as 'us' | 'uk', reportYear); + builder.addAdult('you', 30, { employment_income: 0 }); + setHouseholdDraft(builder.build()); + setHouseholdLabel(''); + setIsCreationMode(true); + }, [countryId, reportYear]); + + // Exit creation mode (back to browse) + const handleExitCreationMode = useCallback(() => { + setIsCreationMode(false); + setHouseholdDraft(null); + setHouseholdLabel(''); + }, []); + + // Handle marital status change + const handleMaritalStatusChange = useCallback( + (newStatus: 'single' | 'married') => { + if (!householdDraft) return; + + const builder = new HouseholdBuilder(countryId as 'us' | 'uk', reportYear); + builder.loadHousehold(householdDraft); + + const hasPartner = householdPeople.includes('your partner'); + + if (newStatus === 'married' && !hasPartner) { + builder.addAdult('your partner', 30, { employment_income: 0 }); + builder.setMaritalStatus('you', 'your partner'); + } else if (newStatus === 'single' && hasPartner) { + builder.removePerson('your partner'); + } + + setHouseholdDraft(builder.build()); + }, + [householdDraft, householdPeople, countryId, reportYear] + ); + + // Handle number of children change + const handleNumChildrenChange = useCallback( + (newCount: number) => { + if (!householdDraft) return; + + const builder = new HouseholdBuilder(countryId as 'us' | 'uk', reportYear); + builder.loadHousehold(householdDraft); + + const currentChildren = householdPeople.filter((p) => p.includes('dependent')); + const currentChildCount = currentChildren.length; + + if (newCount !== currentChildCount) { + // Remove all existing children + currentChildren.forEach((child) => builder.removePerson(child)); + + // Add new children + if (newCount > 0) { + const hasPartner = householdPeople.includes('your partner'); + const parentIds = hasPartner ? ['you', 'your partner'] : ['you']; + const ordinals = ['first', 'second', 'third', 'fourth', 'fifth']; + + for (let i = 0; i < newCount; i++) { + const childName = `your ${ordinals[i] || `${i + 1}th`} dependent`; + builder.addChild(childName, 10, parentIds, { employment_income: 0 }); + } + } + } + + setHouseholdDraft(builder.build()); + }, + [householdDraft, householdPeople, countryId, reportYear] + ); + + // Handle household creation submission + const handleCreateHousehold = useCallback(async () => { + if (!householdDraft || !householdLabel.trim()) { + return; + } + + const payload = HouseholdAdapter.toCreationPayload(householdDraft.householdData, countryId); + + try { + const result = await createHousehold(payload); + const householdId = result.result.household_id.toString(); + + // Record usage + householdUsageStore.recordUsage(householdId); + + // Create household with ID set for proper selection highlighting + const createdHousehold: Household = { + ...householdDraft, + id: householdId, + }; + + const populationState = { + geography: null, + household: createdHousehold, + label: householdLabel, + type: 'household' as const, + }; + + // Wait for the household associations query to refetch so the new household appears in recentPopulations + await queryClient.refetchQueries({ + queryKey: householdAssociationKeys.byUser(userId, countryId), + }); + + // Select the newly created household + onSelect(populationState); + onClose(); + } catch (err) { + console.error('Failed to create household:', err); + } + }, [ + householdDraft, + householdLabel, + countryId, + createHousehold, + onSelect, + onClose, + queryClient, + userId, + ]); + + const colorConfig = INGREDIENT_COLORS.population; + const countryConfig = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + + // Styles (matching PolicyBrowseModal) + const modalStyles = { + sidebar: { + width: 220, + borderRight: `1px solid ${colors.border.light}`, + display: 'flex', + flexDirection: 'column' as const, + flexShrink: 0, + overflow: 'hidden' as const, + }, + sidebarInner: { + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.lg, + padding: spacing.md, + paddingRight: spacing.lg, + }, + sidebarSection: { + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.xs, + }, + sidebarLabel: { + fontSize: FONT_SIZES.small, + fontWeight: typography.fontWeight.semibold, + color: colors.gray[500], + padding: `0 ${spacing.sm}`, + marginBottom: spacing.xs, + }, + sidebarItem: { + display: 'flex', + alignItems: 'center', + gap: spacing.sm, + padding: `${spacing.sm} ${spacing.md}`, + borderRadius: spacing.radius.md, + cursor: 'pointer', + transition: 'all 0.15s ease', + fontSize: FONT_SIZES.small, + fontWeight: typography.fontWeight.medium, + }, + mainContent: { + flex: 1, + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.lg, + minWidth: 0, + padding: spacing.xl, + overflow: 'hidden' as const, + }, + regionGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', + gap: spacing.sm, + }, + regionChip: { + padding: `${spacing.sm} ${spacing.md}`, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + background: colors.white, + cursor: 'pointer', + transition: 'all 0.15s ease', + fontSize: FONT_SIZES.small, + textAlign: 'center' as const, + }, + householdCard: { + padding: spacing.md, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + background: colors.white, + cursor: 'pointer', + transition: 'all 0.15s ease', + }, + }; + + // Dock styles for creation mode status header + const dockStyles = { + statusHeader: { + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + borderRadius: spacing.radius.lg, + border: `1px solid ${householdDraft ? colorConfig.border : colors.border.light}`, + boxShadow: householdDraft + ? `0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px ${colorConfig.border}` + : `0 2px 12px ${colors.shadow.light}`, + padding: `${spacing.sm} ${spacing.lg}`, + transition: 'all 0.3s ease', + marginBottom: spacing.md, + }, + }; + + // Get section title + const getSectionTitle = () => { + if (activeCategory === 'national') return countryId === 'uk' ? 'UK-wide' : 'Nationwide'; + if (activeCategory === 'my-households') return 'My households'; + const category = geographyCategories.find((c) => c.id === activeCategory); + return category?.label || 'Regions'; + }; + + // Get item count for display + const getItemCount = () => { + if (activeCategory === 'national') return 1; + if (activeCategory === 'my-households') return filteredHouseholds.length; + return filteredRegions.length; + }; + + return ( + + + + + + + {isCreationMode ? 'Create household' : 'Household(s)'} + + + {isCreationMode + ? 'Configure your household composition and details' + : 'Choose a geographic region or create a household'} + + + + } + size="90vw" + radius="lg" + styles={{ + content: { + maxWidth: '1400px', + height: '85vh', + maxHeight: '800px', + display: 'flex', + flexDirection: 'column', + }, + header: { + borderBottom: `1px solid ${colors.border.light}`, + paddingBottom: spacing.md, + paddingLeft: spacing.xl, + paddingRight: spacing.xl, + }, + body: { + padding: 0, + flex: 1, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }, + }} + > + + {/* Left Sidebar - independently scrollable */} + + + + {/* Quick Select */} + + Quick select + { + setActiveCategory('national'); + setIsCreationMode(false); + }} + > + {countryId === 'uk' ? : } + + {countryId === 'uk' ? 'UK-wide' : 'Nationwide'} + + + + + + + {/* Geography Categories */} + + Geographies + {geographyCategories.map((category) => ( + { + setActiveCategory(category.id); + setIsCreationMode(false); + }} + > + + {category.label} + + {category.count} + + + ))} + + + + + {/* My Households */} + + Households + { + setActiveCategory('my-households'); + setIsCreationMode(false); + }} + > + + My households + + {sortedHouseholds.length} + + + + {/* Create New - styled as sidebar tab */} + + + Create new household + + + + + + + {/* Main Content Area */} + + {isCreationMode ? ( + // Household Creation Form + <> + {/* Status Header Bar */} + + + {/* Left side: Household icon and editable name */} + + {/* Household icon */} + + + + + {/* Editable household name */} + + {isEditingLabel ? ( + setHouseholdLabel(e.currentTarget.value)} + onBlur={() => setIsEditingLabel(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') setIsEditingLabel(false); + if (e.key === 'Escape') setIsEditingLabel(false); + }} + autoFocus + placeholder="Enter household name..." + size="xs" + style={{ width: 250 }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + border: 'none', + background: 'transparent', + padding: 0, + }, + }} + /> + ) : ( + <> + setIsEditingLabel(true)} + > + {householdLabel || 'Click to name your household...'} + + setIsEditingLabel(true)} + style={{ flexShrink: 0 }} + > + + + + )} + + + + {/* Right side: Member count */} + + + {householdPeople.length > 0 ? ( + <> + + + {householdPeople.length} member{householdPeople.length !== 1 ? 's' : ''} + + + ) : ( + + No members yet + + )} + + + + + + + {/* HouseholdBuilderForm */} + {householdDraft && ( + + + + + )} + + + ) : ( + <> + {/* Search Bar */} + {activeCategory !== 'national' && ( + } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + size="sm" + styles={{ + input: { + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + fontSize: FONT_SIZES.small, + '&:focus': { + borderColor: colorConfig.accent, + }, + }, + }} + /> + )} + + {/* Section Header */} + + + {getSectionTitle()} + + + {getItemCount()} {getItemCount() === 1 ? 'option' : 'options'} + + + + {/* Content */} + + {activeCategory === 'national' ? ( + // National selection - single prominent option + + handleSelectGeography(null)} + > + + {countryId === 'uk' ? ( + + ) : ( + + )} + + + {countryId === 'uk' ? 'Households UK-wide' : 'Households nationwide'} + + + Simulate policy effects across the entire{' '} + {countryId === 'uk' ? 'United Kingdom' : 'United States'} + + + + + + + ) : activeCategory === 'my-households' ? ( + // Households list + householdsLoading ? ( + + {[1, 2, 3].map((i) => ( + + ))} + + ) : filteredHouseholds.length === 0 ? ( + + + + + + {searchQuery ? 'No households match your search' : 'No households yet'} + + + {searchQuery + ? 'Try adjusting your search terms' + : 'Create a custom household using the button in the sidebar'} + + + ) : ( + + {filteredHouseholds.map((household) => ( + handleSelectHousehold(household)} + > + + + + + + + + {household.label} + + + {household.memberCount}{' '} + {household.memberCount === 1 ? 'member' : 'members'} + + + + + + + ))} + + ) + ) : // Geography grid + filteredRegions.length === 0 ? ( + + + No regions match your search + + + ) : ( + + {filteredRegions.map((region) => ( + handleSelectGeography(region)} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = colorConfig.border; + e.currentTarget.style.background = colorConfig.bg; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = colors.border.light; + e.currentTarget.style.background = colors.white; + }} + > + {region.label} + + ))} + + )} + + + )} + + + + {/* Footer for creation mode - fixed at bottom */} + {isCreationMode && ( + + + + + + + )} + + ); +} + +// PolicyCreationModal is imported from ./reportBuilder/modals + +// ============================================================================ +// SIMULATION CANVAS +// ============================================================================ + +// State for the policy browse modal +interface PolicyBrowseState { + isOpen: boolean; + simulationIndex: number; +} + +interface SimulationCanvasProps { + reportState: ReportBuilderState; + setReportState: React.Dispatch>; + pickerState: IngredientPickerState; + setPickerState: React.Dispatch>; + viewMode: ViewMode; +} + +function SimulationCanvas({ + reportState, + setReportState, + pickerState, + setPickerState, + viewMode, +}: SimulationCanvasProps) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const countryConfig = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + const userId = MOCK_USER_ID.toString(); + const { data: policies, isLoading: policiesLoading } = useUserPolicies(userId); + const { data: households } = useUserHouseholds(userId); + const regionOptions = useSelector((state: RootState) => state.metadata.economyOptions.region); + // Any geography selection (nationwide or subnational) requires dual-simulation + // Only households allow single-simulation reports + const isGeographySelected = !!reportState.simulations[0]?.population?.geography?.id; + + // State for the augmented policy browse modal + const [policyBrowseState, setPolicyBrowseState] = useState({ + isOpen: false, + simulationIndex: 0, + }); + + // State for the policy creation modal + const [policyCreationState, setPolicyCreationState] = useState({ + isOpen: false, + simulationIndex: 0, + }); + + // State for the population browse modal + const [populationBrowseState, setPopulationBrowseState] = useState({ + isOpen: false, + simulationIndex: 0, + }); + + // Transform policies data into SavedPolicy format, sorted by most recent + // Uses association data for display (like Policies page), policy data only for param count + const savedPolicies: SavedPolicy[] = useMemo(() => { + return (policies || []) + .map((p) => { + const policyId = p.association.policyId.toString(); + const label = p.association.label || `Policy #${policyId}`; + return { + id: policyId, + label, + paramCount: countPolicyModifications(p.policy), // Handles undefined gracefully + createdAt: p.association.createdAt, + updatedAt: p.association.updatedAt, + }; + }) + .sort((a, b) => { + // Sort by most recent timestamp (prefer updatedAt, fallback to createdAt) + const aTime = a.updatedAt || a.createdAt || ''; + const bTime = b.updatedAt || b.createdAt || ''; + return bTime.localeCompare(aTime); // Descending (most recent first) + }); + }, [policies]); + + // Build recent populations from usage tracking + const recentPopulations: RecentPopulation[] = useMemo(() => { + const results: Array = []; + + // Get all region options for lookup + const regions = regionOptions || []; + const allRegions: RegionOption[] = + countryId === 'us' + ? [...getUSStates(regions), ...getUSCongressionalDistricts(regions)] + : [ + ...getUKCountries(regions), + ...getUKConstituencies(regions), + ...getUKLocalAuthorities(regions), + ]; + + // Add recent geographies (excluding national - that's shown separately) + const recentGeoIds = geographyUsageStore.getRecentIds(10); + for (const geoId of recentGeoIds) { + // Skip national scopes as they're shown separately + if (geoId === 'us' || geoId === 'uk') continue; + + const timestamp = geographyUsageStore.getLastUsed(geoId) || ''; + const region = allRegions.find((r) => r.value === geoId); + + if (region) { + const geographyId = `${countryId}-${geoId}`; + const geography: Geography = { + id: geographyId, + countryId, + scope: 'subnational', + geographyId: geoId, + }; + results.push({ + id: geographyId, // Use full id for matching with currentPopulationId + label: region.label, + type: 'geography', + population: { + geography, + household: null, + label: generateGeographyLabel(geography), + type: 'geography', + }, + timestamp, + }); + } + } + + // Add recent households + const recentHouseholdIds = householdUsageStore.getRecentIds(10); + for (const householdId of recentHouseholdIds) { + const timestamp = householdUsageStore.getLastUsed(householdId) || ''; + + // Find the household in the fetched data (use String() for type-safe comparison) + const householdData = households?.find( + (h) => String(h.association.householdId) === householdId + ); + if (householdData?.household) { + const household = HouseholdAdapter.fromMetadata(householdData.household); + // Use the household.id from the adapter for consistent matching with currentPopulationId + const resolvedId = household.id || householdId; + results.push({ + id: resolvedId, + label: householdData.association.label || `Household #${householdId}`, + type: 'household', + population: { + geography: null, + household, + label: householdData.association.label || `Household #${householdId}`, + type: 'household', + }, + timestamp, + }); + } + } + + // Sort by timestamp (most recent first) and return without timestamp + return results + .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) + .slice(0, 10) + .map(({ timestamp: _t, ...rest }) => rest); + }, [countryId, households, regionOptions]); + + const handleAddSimulation = useCallback(() => { + if (reportState.simulations.length >= 2) return; + const newSim = initializeSimulationState(); + newSim.label = 'Reform simulation'; + newSim.population = { ...reportState.simulations[0].population }; + setReportState((prev) => ({ ...prev, simulations: [...prev.simulations, newSim] })); + }, [reportState.simulations, setReportState]); + + const handleRemoveSimulation = useCallback( + (index: number) => { + if (index === 0) return; + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.filter((_, i) => i !== index), + })); + }, + [setReportState] + ); + + const handleSimulationLabelChange = useCallback( + (index: number, label: string) => { + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => (i === index ? { ...sim, label } : sim)), + })); + }, + [setReportState] + ); + + const handleIngredientSelect = useCallback( + (item: PolicyStateProps | PopulationStateProps | null) => { + const { simulationIndex, ingredientType } = pickerState; + setReportState((prev) => { + const newSimulations = prev.simulations.map((sim, i) => { + if (i !== simulationIndex) return sim; + if (ingredientType === 'policy') return { ...sim, policy: item as PolicyStateProps }; + if (ingredientType === 'population') + return { ...sim, population: item as PopulationStateProps }; + return sim; + }); + if (ingredientType === 'population' && simulationIndex === 0 && newSimulations.length > 1) { + newSimulations[1] = { + ...newSimulations[1], + population: { ...(item as PopulationStateProps) }, + }; + } + return { ...prev, simulations: newSimulations }; + }); + }, + [pickerState, setReportState] + ); + + const handleQuickSelectPolicy = useCallback( + (simulationIndex: number) => { + const policyState: PolicyStateProps = { + id: 'current-law', + label: 'Current law', + parameters: [], + }; + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, policy: policyState } : sim + ), + })); + }, + [setReportState] + ); + + const handleSelectSavedPolicy = useCallback( + (simulationIndex: number, policyId: string, label: string, paramCount: number) => { + const policyState: PolicyStateProps = { + id: policyId, + label, + parameters: Array(paramCount).fill({}), + }; + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, policy: policyState } : sim + ), + })); + }, + [setReportState] + ); + + const handleBrowseMorePolicies = useCallback((simulationIndex: number) => { + // Open the augmented policy browse modal + setPolicyBrowseState({ + isOpen: true, + simulationIndex, + }); + }, []); + + // Handle policy selection from the browse modal + const handlePolicySelectFromBrowse = useCallback( + (policy: PolicyStateProps) => { + const { simulationIndex } = policyBrowseState; + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, policy } : sim + ), + })); + }, + [policyBrowseState, setReportState] + ); + + const handleBrowseMorePopulations = useCallback((simulationIndex: number) => { + // Open the augmented population browse modal + setPopulationBrowseState({ + isOpen: true, + simulationIndex, + }); + }, []); + + // Handle population selection from the browse modal + const handlePopulationSelectFromBrowse = useCallback( + (population: PopulationStateProps) => { + const { simulationIndex } = populationBrowseState; + + setReportState((prev) => { + // Create a new population object to ensure React detects the change + const newPopulation = { ...population }; + + let newSimulations = prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, population: newPopulation } : sim + ); + + // If updating baseline population, also update reform's inherited population + if (simulationIndex === 0 && newSimulations.length > 1) { + newSimulations[1] = { ...newSimulations[1], population: { ...newPopulation } }; + } + + return { ...prev, simulations: newSimulations }; + }); + }, + [populationBrowseState, setReportState] + ); + + const handleQuickSelectPopulation = useCallback( + (simulationIndex: number, populationType: 'nationwide') => { + const samplePopulations = getSamplePopulations(countryId); + const populationState = samplePopulations.nationwide; + + // Record usage for the geography + if (populationState.geography?.geographyId) { + geographyUsageStore.recordUsage(populationState.geography.geographyId); + } + + setReportState((prev) => { + let newSimulations = prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, population: { ...populationState } } : sim + ); + + // Update reform's inherited population if baseline + if (simulationIndex === 0 && newSimulations.length > 1) { + newSimulations[1] = { ...newSimulations[1], population: { ...populationState } }; + } + + return { ...prev, simulations: newSimulations }; + }); + }, + [countryId, setReportState] + ); + + // Handle selection from recent populations + const handleSelectRecentPopulation = useCallback( + (simulationIndex: number, population: PopulationStateProps) => { + // Record usage + if (population.geography?.geographyId) { + geographyUsageStore.recordUsage(population.geography.geographyId); + } else if (population.household?.id) { + householdUsageStore.recordUsage(population.household.id); + } + + setReportState((prev) => { + // Create a new population object to ensure React detects the change + const newPopulation = { ...population }; + + let newSimulations = prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, population: newPopulation } : sim + ); + + // Update reform's inherited population if baseline + if (simulationIndex === 0 && newSimulations.length > 1) { + newSimulations[1] = { ...newSimulations[1], population: { ...newPopulation } }; + } + + return { ...prev, simulations: newSimulations }; + }); + }, + [setReportState] + ); + + const handleDeselectPolicy = useCallback( + (simulationIndex: number) => { + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, policy: initializePolicyState() } : sim + ), + })); + }, + [setReportState] + ); + + const handleDeselectPopulation = useCallback( + (simulationIndex: number) => { + setReportState((prev) => { + let newSimulations = prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, population: initializePopulationState() } : sim + ); + + // If deselecting baseline population, also clear reform's inherited population + if (simulationIndex === 0 && newSimulations.length > 1) { + newSimulations[1] = { + ...newSimulations[1], + population: initializePopulationState(), + }; + } + + return { ...prev, simulations: newSimulations }; + }); + }, + [setReportState] + ); + + const handleCreateCustom = useCallback( + (simulationIndex: number, ingredientType: IngredientType) => { + if (ingredientType === 'policy') { + // Open the policy creation modal instead of redirecting + setPolicyCreationState({ isOpen: true, simulationIndex }); + } else if (ingredientType === 'population') { + window.location.href = `/${countryId}/households/create`; + } + }, + [countryId] + ); + + // Handle policy created from the creation modal + const handlePolicyCreated = useCallback( + (simulationIndex: number, policy: PolicyStateProps) => { + setReportState((prev) => { + const newSimulations = [...prev.simulations]; + if (newSimulations[simulationIndex]) { + newSimulations[simulationIndex] = { + ...newSimulations[simulationIndex], + policy: { + id: policy.id, + label: policy.label, + parameters: policy.parameters, + }, + }; + } + return { ...prev, simulations: newSimulations }; + }); + }, + [setReportState] + ); + + return ( + <> + + + + handleSimulationLabelChange(0, label)} + onQuickSelectPolicy={() => handleQuickSelectPolicy(0)} + onSelectSavedPolicy={(id, label, paramCount) => + handleSelectSavedPolicy(0, id, label, paramCount) + } + onQuickSelectPopulation={() => handleQuickSelectPopulation(0, 'nationwide')} + onSelectRecentPopulation={(pop) => handleSelectRecentPopulation(0, pop)} + onDeselectPolicy={() => handleDeselectPolicy(0)} + onDeselectPopulation={() => handleDeselectPopulation(0)} + onCreateCustomPolicy={() => handleCreateCustom(0, 'policy')} + onBrowseMorePolicies={() => handleBrowseMorePolicies(0)} + onBrowseMorePopulations={() => handleBrowseMorePopulations(0)} + canRemove={false} + savedPolicies={savedPolicies} + recentPopulations={recentPopulations} + viewMode={viewMode} + /> + + {reportState.simulations.length > 1 ? ( + handleSimulationLabelChange(1, label)} + onQuickSelectPolicy={() => handleQuickSelectPolicy(1)} + onSelectSavedPolicy={(id, label, paramCount) => + handleSelectSavedPolicy(1, id, label, paramCount) + } + onQuickSelectPopulation={() => handleQuickSelectPopulation(1, 'nationwide')} + onSelectRecentPopulation={(pop) => handleSelectRecentPopulation(1, pop)} + onDeselectPolicy={() => handleDeselectPolicy(1)} + onDeselectPopulation={() => handleDeselectPopulation(1)} + onCreateCustomPolicy={() => handleCreateCustom(1, 'policy')} + onBrowseMorePolicies={() => handleBrowseMorePolicies(1)} + onBrowseMorePopulations={() => handleBrowseMorePopulations(1)} + onRemove={() => handleRemoveSimulation(1)} + canRemove={!isGeographySelected} + isRequired={isGeographySelected} + populationInherited={true} + inheritedPopulation={reportState.simulations[0].population} + savedPolicies={savedPolicies} + recentPopulations={recentPopulations} + viewMode={viewMode} + /> + ) : ( + + )} + + + + setPickerState((prev) => ({ ...prev, isOpen: false }))} + type={pickerState.ingredientType} + onSelect={handleIngredientSelect} + onCreateNew={() => + handleCreateCustom(pickerState.simulationIndex, pickerState.ingredientType) + } + /> + + {/* Augmented Policy Browse Modal */} + setPolicyBrowseState((prev) => ({ ...prev, isOpen: false }))} + onSelect={handlePolicySelectFromBrowse} + /> + + {/* Augmented Population Browse Modal */} + setPopulationBrowseState((prev) => ({ ...prev, isOpen: false }))} + onSelect={handlePopulationSelectFromBrowse} + onCreateNew={() => handleCreateCustom(populationBrowseState.simulationIndex, 'population')} + /> + + {/* Policy Creation Modal */} + setPolicyCreationState((prev) => ({ ...prev, isOpen: false }))} + onPolicyCreated={(policy) => + handlePolicyCreated(policyCreationState.simulationIndex, policy) + } + simulationIndex={policyCreationState.simulationIndex} + /> + + ); +} + +// ============================================================================ +// REPORT META PANEL - Floating Toolbar / Dock Design +// ============================================================================ + +interface ReportMetaPanelProps { + reportState: ReportBuilderState; + setReportState: React.Dispatch>; + isReportConfigured: boolean; +} + +// Progress dot component +function ProgressDot({ filled, pulsing }: { filled: boolean; pulsing?: boolean }) { + return ( + + ); +} + +function ReportMetaPanel({ + reportState, + setReportState, + isReportConfigured, +}: ReportMetaPanelProps) { + const [isEditingLabel, setIsEditingLabel] = useState(false); + const [labelInput, setLabelInput] = useState(''); + + const handleLabelSubmit = () => { + setReportState((prev) => ({ ...prev, label: labelInput || 'Untitled report' })); + setIsEditingLabel(false); + }; + + // Calculate configuration progress + const simulations = reportState.simulations; + const baselinePolicyConfigured = !!simulations[0]?.policy?.id; + const baselinePopulationConfigured = !!( + simulations[0]?.population?.household?.id || simulations[0]?.population?.geography?.id + ); + const hasReform = simulations.length > 1; + const reformPolicyConfigured = hasReform && !!simulations[1]?.policy?.id; + + // Get labels for display + const baselinePolicyLabel = simulations[0]?.policy?.label || null; + const baselinePopulationLabel = + simulations[0]?.population?.label || + (simulations[0]?.population?.household?.id + ? 'Household' + : simulations[0]?.population?.geography?.id + ? 'Nationwide' + : null); + const reformPolicyLabel = hasReform ? simulations[1]?.policy?.label || null : null; + + // Progress steps + const steps = [ + baselinePolicyConfigured, + baselinePopulationConfigured, + ...(hasReform ? [reformPolicyConfigured] : []), + ]; + const completedSteps = steps.filter(Boolean).length; + + // Floating dock styles - matches canvas container styling + const dockStyles = { + container: { + marginBottom: spacing.xl, + }, + dock: { + background: 'rgba(255, 255, 255, 0.92)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + borderRadius: spacing.radius.xl, // Match canvasContainer + border: `1px solid ${isReportConfigured ? colors.primary[200] : colors.border.light}`, + boxShadow: isReportConfigured + ? `0 8px 32px rgba(44, 122, 123, 0.15), 0 2px 8px rgba(0, 0, 0, 0.08)` + : `0 4px 24px ${colors.shadow.light}`, // Match canvasContainer shadow + padding: `${spacing.md} ${spacing.xl}`, + transition: 'all 0.35s cubic-bezier(0.4, 0, 0.2, 1)', + cursor: 'default', + overflow: 'hidden', + }, + compactRow: { + display: 'flex', + alignItems: 'center', + gap: spacing.md, + width: '100%', + }, + expandedContent: { + marginTop: spacing.md, + paddingTop: spacing.md, + borderTop: `1px solid ${colors.gray[200]}`, + }, + divider: { + width: '1px', + height: '24px', + background: colors.gray[200], + margin: `0 ${spacing.xs}`, + }, + runButton: { + background: isReportConfigured + ? `linear-gradient(135deg, ${colors.primary[500]} 0%, ${colors.primary[600]} 100%)` + : colors.gray[200], + color: isReportConfigured ? 'white' : colors.gray[500], + border: 'none', + borderRadius: spacing.radius.lg, // Match other buttons + padding: `${spacing.sm} ${spacing.lg}`, + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + cursor: isReportConfigured ? 'pointer' : 'not-allowed', + display: 'flex', + alignItems: 'center', + gap: spacing.xs, + transition: 'all 0.3s ease', + boxShadow: isReportConfigured ? `0 4px 12px rgba(44, 122, 123, 0.3)` : 'none', + }, + configRow: { + display: 'flex', + alignItems: 'center', + gap: spacing.sm, + padding: `${spacing.xs} 0`, + }, + configChip: { + display: 'flex', + alignItems: 'center', + gap: spacing.xs, + padding: `${spacing.xs} ${spacing.sm}`, + background: colors.gray[50], + borderRadius: spacing.radius.md, + fontFamily: typography.fontFamily.primary, + fontSize: FONT_SIZES.small, + }, + }; + + return ( + + + {/* Compact row - always visible */} + + {/* Document icon */} + + + + + {/* Title with pencil icon - flexible width */} + + {isEditingLabel ? ( + setLabelInput(e.target.value)} + onBlur={handleLabelSubmit} + onKeyDown={(e) => e.key === 'Enter' && handleLabelSubmit()} + placeholder="Report name..." + size="xs" + autoFocus + style={{ flex: 1 }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + border: 'none', + background: 'transparent', + padding: 0, + }, + }} + /> + ) : ( + <> + + {reportState.label || 'Untitled report'} + + { + setLabelInput(reportState.label || ''); + setIsEditingLabel(true); + }} + aria-label="Edit report name" + style={{ flexShrink: 0 }} + > + + + + )} + + + {/* Divider */} + + + {/* Year selector - fixed width, no checkmark */} + setReportState((prev) => ({ ...prev, year: value || CURRENT_YEAR }))} + data={['2023', '2024', '2025', '2026']} + size="sm" + variant="unstyled" + withCheckIcon={false} + disabled={isReadOnly} + rightSection={isReadOnly ? : undefined} + comboboxProps={{ width: 120, position: 'bottom-end' }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontSize: FONT_SIZES.small, + fontWeight: 500, + color: colors.gray[700], + cursor: isReadOnly ? 'default' : 'pointer', + paddingLeft: 0, + paddingRight: isReadOnly ? 0 : spacing.xl, + minHeight: 'auto', + height: 'auto', + WebkitTextFillColor: colors.gray[700], + background: 'transparent', + }, + root: { + width: isReadOnly ? 'auto' : 72, + }, + }} + /> + + + ); +} diff --git a/app/src/pages/reportBuilder/components/SimulationBlock.tsx b/app/src/pages/reportBuilder/components/SimulationBlock.tsx new file mode 100644 index 000000000..0d166ecff --- /dev/null +++ b/app/src/pages/reportBuilder/components/SimulationBlock.tsx @@ -0,0 +1,187 @@ +/** + * SimulationBlock - A simulation configuration card + */ + +import { IconCheck, IconTrash } from '@tabler/icons-react'; +import { ActionIcon, Box, Group, Paper, Text, Tooltip } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import type { PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; +import { FONT_SIZES } from '../constants'; +import { styles } from '../styles'; +import type { RecentPopulation, SavedPolicy } from '../types'; +import { EditableLabel } from './EditableLabel'; +import { IngredientSection } from './IngredientSection'; + +export interface SimulationBlockProps { + simulation: SimulationStateProps; + index: number; + countryId: 'us' | 'uk'; + onLabelChange: (label: string) => void; + onQuickSelectPolicy: () => void; + onSelectSavedPolicy: (id: string, label: string, paramCount: number) => void; + onQuickSelectPopulation: (populationType: 'nationwide') => void; + onSelectRecentPopulation: (population: PopulationStateProps) => void; + onDeselectPolicy: () => void; + onDeselectPopulation: () => void; + onEditPolicy: () => void; + onViewPolicy: () => void; + onCreateCustomPolicy: () => void; + onBrowseMorePolicies: () => void; + onBrowseMorePopulations: () => void; + onRemove?: () => void; + canRemove: boolean; + isRequired?: boolean; + populationInherited?: boolean; + inheritedPopulation?: PopulationStateProps | null; + savedPolicies: SavedPolicy[]; + recentPopulations: RecentPopulation[]; + isReadOnly?: boolean; +} + +export function SimulationBlock({ + simulation, + index, + countryId, + onLabelChange, + onQuickSelectPolicy, + onSelectSavedPolicy, + onQuickSelectPopulation, + onSelectRecentPopulation, + onEditPolicy, + onViewPolicy: _onViewPolicy, + onDeselectPolicy, + onDeselectPopulation, + onBrowseMorePolicies, + onBrowseMorePopulations, + onRemove, + canRemove, + isRequired, + populationInherited, + inheritedPopulation, + savedPolicies, + recentPopulations, +}: SimulationBlockProps) { + const isPolicyConfigured = !!simulation.policy.id; + const effectivePopulation = + populationInherited && inheritedPopulation ? inheritedPopulation : simulation.population; + const isPopulationConfigured = !!( + effectivePopulation?.household?.id || effectivePopulation?.geography?.id + ); + const isFullyConfigured = isPolicyConfigured && isPopulationConfigured; + + const defaultLabel = index === 0 ? 'Baseline simulation' : 'Reform simulation'; + + const currentPolicyId = simulation.policy.id; + const currentPopulationId = + effectivePopulation?.household?.id || effectivePopulation?.geography?.id; + + // Determine inherited population type for display + const inheritedPopulationType = + populationInherited && inheritedPopulation + ? inheritedPopulation.household?.id + ? 'household' + : inheritedPopulation.geography?.id + ? inheritedPopulation.geography.scope === 'subnational' + ? 'subnational' + : 'nationwide' + : null + : null; + + const inheritedPopulationLabel = + populationInherited && inheritedPopulation + ? inheritedPopulation.label || inheritedPopulation.geography?.name || undefined + : undefined; + + return ( + + {/* Status indicator */} + + + {/* Header */} + + + + + {isRequired && ( + + Required + + )} + {isFullyConfigured && ( + + + + + + )} + {canRemove && ( + + + + )} + + + + {/* Panels - direct children for subgrid alignment */} + {}} + onBrowseMore={onBrowseMorePolicies} + savedPolicies={savedPolicies} + /> + + {}} + onBrowseMore={onBrowseMorePopulations} + isInherited={populationInherited} + inheritedPopulationType={inheritedPopulationType} + inheritedPopulationLabel={inheritedPopulationLabel} + recentPopulations={recentPopulations} + /> + + ); +} diff --git a/app/src/pages/reportBuilder/components/SimulationBlockFull.tsx b/app/src/pages/reportBuilder/components/SimulationBlockFull.tsx new file mode 100644 index 000000000..bb75f11fe --- /dev/null +++ b/app/src/pages/reportBuilder/components/SimulationBlockFull.tsx @@ -0,0 +1,176 @@ +/** + * SimulationBlockFull - Simulation card with full-width ingredient sections + * + * Same structure as SimulationBlock but uses IngredientSectionFull + * for a layout where each ingredient fills its entire section. + */ + +import { IconCheck, IconTrash } from '@tabler/icons-react'; +import { ActionIcon, Box, Group, Paper, Text, Tooltip } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { FONT_SIZES } from '../constants'; +import { styles } from '../styles'; +import { EditableLabel } from './EditableLabel'; +import { IngredientSectionFull } from './IngredientSectionFull'; +import type { SimulationBlockProps } from './SimulationBlock'; + +export function SimulationBlockFull({ + simulation, + index, + countryId, + onLabelChange, + onQuickSelectPolicy, + onSelectSavedPolicy, + onQuickSelectPopulation, + onSelectRecentPopulation, + onEditPolicy, + onViewPolicy, + onDeselectPolicy, + onDeselectPopulation, + onBrowseMorePolicies, + onBrowseMorePopulations, + onRemove, + canRemove, + isRequired, + populationInherited, + inheritedPopulation, + savedPolicies, + recentPopulations, + isReadOnly, +}: SimulationBlockProps) { + const isPolicyConfigured = !!simulation.policy.id; + const effectivePopulation = + populationInherited && inheritedPopulation ? inheritedPopulation : simulation.population; + const isPopulationConfigured = !!( + effectivePopulation?.household?.id || effectivePopulation?.geography?.id + ); + const isFullyConfigured = isPolicyConfigured && isPopulationConfigured; + + const defaultLabel = index === 0 ? 'Baseline simulation' : 'Reform simulation'; + + const currentPolicyId = simulation.policy.id; + const currentPopulationId = + effectivePopulation?.household?.id || effectivePopulation?.geography?.id; + const populationLabel = + effectivePopulation?.label || effectivePopulation?.geography?.name || undefined; + + const inheritedPopulationType = + populationInherited && inheritedPopulation + ? inheritedPopulation.household?.id + ? 'household' + : inheritedPopulation.geography?.id + ? inheritedPopulation.geography.scope === 'subnational' + ? 'subnational' + : 'nationwide' + : null + : null; + + const inheritedPopulationLabel = + populationInherited && inheritedPopulation + ? inheritedPopulation.label || inheritedPopulation.geography?.name || undefined + : undefined; + + return ( + + {/* Status indicator */} + + + {/* Header */} + + {isReadOnly ? ( + + {simulation.label || defaultLabel} + + ) : ( + + )} + + + {isRequired && ( + + Required + + )} + {isFullyConfigured && ( + + + + + + )} + {canRemove && !isReadOnly && ( + + + + )} + + + + {/* Full-width ingredient sections */} + {}} + onBrowseMore={onBrowseMorePolicies} + savedPolicies={savedPolicies} + isReadOnly={isReadOnly} + /> + + {}} + onBrowseMore={onBrowseMorePopulations} + isInherited={populationInherited} + inheritedPopulationType={inheritedPopulationType} + inheritedPopulationLabel={inheritedPopulationLabel} + recentPopulations={recentPopulations} + isReadOnly={isReadOnly} + /> + + ); +} diff --git a/app/src/pages/reportBuilder/components/SimulationCanvas.tsx b/app/src/pages/reportBuilder/components/SimulationCanvas.tsx new file mode 100644 index 000000000..4409652a9 --- /dev/null +++ b/app/src/pages/reportBuilder/components/SimulationCanvas.tsx @@ -0,0 +1,148 @@ +/** + * SimulationCanvas - Renders simulation blocks and modals + * + * This is a thin render layer. All state management, data fetching, + * and callback logic lives in useSimulationCanvas. + */ + +import { Box } from '@mantine/core'; +import { useSimulationCanvas } from '../hooks/useSimulationCanvas'; +import { + IngredientPickerModal, + PolicyBrowseModal, + PolicyCreationModal, + PopulationBrowseModal, +} from '../modals'; +import { styles } from '../styles'; +import type { IngredientPickerState, ReportBuilderState } from '../types'; +import { AddSimulationCard } from './AddSimulationCard'; +import { SimulationBlock, type SimulationBlockProps } from './SimulationBlock'; +import { SimulationCanvasSkeleton } from './SimulationCanvasSkeleton'; + +interface SimulationCanvasProps { + reportState: ReportBuilderState; + setReportState: React.Dispatch>; + pickerState: IngredientPickerState; + setPickerState: React.Dispatch>; + BlockComponent?: React.ComponentType; + isReadOnly?: boolean; +} + +export function SimulationCanvas({ + reportState, + setReportState, + pickerState, + setPickerState, + BlockComponent = SimulationBlock, + isReadOnly, +}: SimulationCanvasProps) { + const canvas = useSimulationCanvas({ reportState, setReportState, pickerState, setPickerState }); + + if (canvas.isInitialLoading) { + return ; + } + + return ( + <> + + + + canvas.handleSimulationLabelChange(0, label)} + onQuickSelectPolicy={() => canvas.handleQuickSelectPolicy(0)} + onSelectSavedPolicy={(id, label, paramCount) => + canvas.handleSelectSavedPolicy(0, id, label, paramCount) + } + onQuickSelectPopulation={() => canvas.handleQuickSelectPopulation(0, 'nationwide')} + onSelectRecentPopulation={(pop) => canvas.handleSelectRecentPopulation(0, pop)} + onDeselectPolicy={() => canvas.handleDeselectPolicy(0)} + onDeselectPopulation={() => canvas.handleDeselectPopulation(0)} + onEditPolicy={() => canvas.handleEditPolicy(0)} + onViewPolicy={() => canvas.handleViewPolicy(0)} + onCreateCustomPolicy={() => canvas.handleCreateCustom(0, 'policy')} + onBrowseMorePolicies={() => canvas.handleBrowseMorePolicies(0)} + onBrowseMorePopulations={() => canvas.handleBrowseMorePopulations(0)} + canRemove={false} + savedPolicies={canvas.savedPolicies} + recentPopulations={canvas.recentPopulations} + isReadOnly={isReadOnly} + /> + + {reportState.simulations.length > 1 ? ( + canvas.handleSimulationLabelChange(1, label)} + onQuickSelectPolicy={() => canvas.handleQuickSelectPolicy(1)} + onSelectSavedPolicy={(id, label, paramCount) => + canvas.handleSelectSavedPolicy(1, id, label, paramCount) + } + onQuickSelectPopulation={() => canvas.handleQuickSelectPopulation(1, 'nationwide')} + onSelectRecentPopulation={(pop) => canvas.handleSelectRecentPopulation(1, pop)} + onDeselectPolicy={() => canvas.handleDeselectPolicy(1)} + onDeselectPopulation={() => canvas.handleDeselectPopulation(1)} + onEditPolicy={() => canvas.handleEditPolicy(1)} + onViewPolicy={() => canvas.handleViewPolicy(1)} + onCreateCustomPolicy={() => canvas.handleCreateCustom(1, 'policy')} + onBrowseMorePolicies={() => canvas.handleBrowseMorePolicies(1)} + onBrowseMorePopulations={() => canvas.handleBrowseMorePopulations(1)} + onRemove={() => canvas.handleRemoveSimulation(1)} + canRemove={!canvas.isGeographySelected} + isRequired={canvas.isGeographySelected} + populationInherited + inheritedPopulation={reportState.simulations[0].population} + savedPolicies={canvas.savedPolicies} + recentPopulations={canvas.recentPopulations} + isReadOnly={isReadOnly} + /> + ) : ( + + )} + + + + + canvas.handleCreateCustom( + canvas.pickerState.simulationIndex, + canvas.pickerState.ingredientType + ) + } + /> + + + + + canvas.handleCreateCustom(canvas.populationBrowseState.simulationIndex, 'population') + } + /> + + + canvas.handlePolicyCreated(canvas.policyCreationState.simulationIndex, policy) + } + simulationIndex={canvas.policyCreationState.simulationIndex} + initialPolicy={canvas.policyCreationState.initialPolicy} + initialEditorMode={canvas.policyCreationState.initialEditorMode} + /> + + ); +} diff --git a/app/src/pages/reportBuilder/components/SimulationCanvasSkeleton.tsx b/app/src/pages/reportBuilder/components/SimulationCanvasSkeleton.tsx new file mode 100644 index 000000000..a1ffc0824 --- /dev/null +++ b/app/src/pages/reportBuilder/components/SimulationCanvasSkeleton.tsx @@ -0,0 +1,105 @@ +/** + * SimulationCanvasSkeleton - Loading placeholder for the simulation canvas + */ + +import { Box, Group, Skeleton } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { styles } from '../styles'; + +export function SimulationCanvasSkeleton() { + return ( + + + + {/* Simulation block skeleton */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Add simulation card skeleton */} + + + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/components/TopBar.tsx b/app/src/pages/reportBuilder/components/TopBar.tsx new file mode 100644 index 000000000..016012ace --- /dev/null +++ b/app/src/pages/reportBuilder/components/TopBar.tsx @@ -0,0 +1,126 @@ +/** + * TopBar - Composable top bar with flexible action buttons + * + * Renders children (typically ReportMetaPanel segments) on the left and + * action buttons on the right. All elements share a consistent 44px height. + */ + +import React from 'react'; +import { Box, Loader } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; +import { FONT_SIZES } from '../constants'; +import type { TopBarAction } from '../types'; + +interface TopBarProps { + children: React.ReactNode; + actions: TopBarAction[]; +} + +function getButtonStyles(action: TopBarAction): React.CSSProperties { + const enabled = !action.disabled && !action.loading; + + if (action.variant === 'primary') { + return { + background: enabled ? colors.primary[500] : colors.gray[200], + color: enabled ? 'white' : colors.gray[500], + border: 'none', + boxShadow: 'none', + cursor: enabled ? 'pointer' : 'not-allowed', + opacity: action.loading ? 0.7 : 1, + }; + } + + return { + background: colors.secondary[100], + color: colors.secondary[700], + border: `1px solid ${colors.secondary[200]}`, + boxShadow: 'none', + cursor: enabled ? 'pointer' : 'not-allowed', + opacity: action.disabled ? 0.5 : 1, + }; +} + +function handleMouseEnter(e: React.MouseEvent, action: TopBarAction) { + const enabled = !action.disabled && !action.loading; + if (!enabled) { + return; + } + + if (action.variant === 'primary') { + e.currentTarget.style.background = colors.primary[600]; + } else { + e.currentTarget.style.background = colors.secondary[200]; + } +} + +function handleMouseLeave(e: React.MouseEvent, action: TopBarAction) { + if (action.variant === 'primary') { + const enabled = !action.disabled && !action.loading; + e.currentTarget.style.background = enabled ? colors.primary[500] : colors.gray[200]; + } else { + e.currentTarget.style.background = colors.secondary[100]; + } +} + +export function TopBar({ children, actions }: TopBarProps) { + return ( + + + {children} + + {actions.length > 0 && ( + + {actions.map((action) => ( + ) => + handleMouseEnter(e, action) + } + onMouseLeave={(e: React.MouseEvent) => + handleMouseLeave(e, action) + } + > + {action.loading ? : action.icon} + {action.loading ? action.loadingLabel || action.label : action.label} + + ))} + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/components/chips/BrowseMoreChip.tsx b/app/src/pages/reportBuilder/components/chips/BrowseMoreChip.tsx new file mode 100644 index 000000000..0b606e45a --- /dev/null +++ b/app/src/pages/reportBuilder/components/chips/BrowseMoreChip.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { IconSearch } from '@tabler/icons-react'; +import { Box, Stack, Text } from '@mantine/core'; +import { colors } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { chipStyles } from '../../styles'; +import { BrowseMoreChipProps } from '../../types'; + +export function BrowseMoreChip({ + label, + description, + onClick, + variant, + colorConfig, +}: BrowseMoreChipProps) { + const [isHovered, setIsHovered] = useState(false); + + if (variant === 'square') { + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + {label} + + {description && ( + + {description} + + )} + + ); + } + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + + + + {label} + + {description && ( + + {description} + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/components/chips/CreateCustomChip.tsx b/app/src/pages/reportBuilder/components/chips/CreateCustomChip.tsx new file mode 100644 index 000000000..9b7d0ad26 --- /dev/null +++ b/app/src/pages/reportBuilder/components/chips/CreateCustomChip.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import { IconPlus } from '@tabler/icons-react'; +import { Box, Text } from '@mantine/core'; +import { colors } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { chipStyles } from '../../styles'; +import { CreateCustomChipProps } from '../../types'; + +export function CreateCustomChip({ label, onClick, variant, colorConfig }: CreateCustomChipProps) { + const [isHovered, setIsHovered] = useState(false); + + if (variant === 'square') { + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + {label} + + + ); + } + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + + + + {label} + + + ); +} diff --git a/app/src/pages/reportBuilder/components/chips/OptionChipRow.tsx b/app/src/pages/reportBuilder/components/chips/OptionChipRow.tsx new file mode 100644 index 000000000..1525873b1 --- /dev/null +++ b/app/src/pages/reportBuilder/components/chips/OptionChipRow.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { IconCheck } from '@tabler/icons-react'; +import { Box, Stack, Text } from '@mantine/core'; +import { colors } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { chipStyles } from '../../styles'; +import { OptionChipRowProps } from '../../types'; + +export function OptionChipRow({ + icon, + label, + description, + isSelected, + onClick, + colorConfig, +}: OptionChipRowProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + {icon} + + + + {label} + + {description && ( + + {description} + + )} + + {isSelected && } + + ); +} diff --git a/app/src/pages/reportBuilder/components/chips/OptionChipSquare.tsx b/app/src/pages/reportBuilder/components/chips/OptionChipSquare.tsx new file mode 100644 index 000000000..d5f6a20b4 --- /dev/null +++ b/app/src/pages/reportBuilder/components/chips/OptionChipSquare.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { Box, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { chipStyles } from '../../styles'; +import { OptionChipSquareProps } from '../../types'; + +export function OptionChipSquare({ + icon, + label, + description, + isSelected, + onClick, + colorConfig, +}: OptionChipSquareProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > + + {icon} + + + {label} + + {description && ( + + {description} + + )} + + ); +} diff --git a/app/src/pages/reportBuilder/components/chips/index.ts b/app/src/pages/reportBuilder/components/chips/index.ts new file mode 100644 index 000000000..ca2064248 --- /dev/null +++ b/app/src/pages/reportBuilder/components/chips/index.ts @@ -0,0 +1,4 @@ +export { OptionChipSquare } from './OptionChipSquare'; +export { OptionChipRow } from './OptionChipRow'; +export { CreateCustomChip } from './CreateCustomChip'; +export { BrowseMoreChip } from './BrowseMoreChip'; diff --git a/app/src/pages/reportBuilder/components/index.ts b/app/src/pages/reportBuilder/components/index.ts new file mode 100644 index 000000000..5804d40b4 --- /dev/null +++ b/app/src/pages/reportBuilder/components/index.ts @@ -0,0 +1,16 @@ +// Chip components +export { OptionChipSquare, OptionChipRow, CreateCustomChip, BrowseMoreChip } from './chips'; + +// Shared components +export { ProgressDot, CountryMapIcon, CreationStatusHeader } from './shared'; + +// Page components +export { IngredientSection } from './IngredientSection'; +export { IngredientSectionFull } from './IngredientSectionFull'; +export { SimulationBlock } from './SimulationBlock'; +export { SimulationBlockFull } from './SimulationBlockFull'; +export { AddSimulationCard } from './AddSimulationCard'; +export { ReportMetaPanel } from './ReportMetaPanel'; +export { SimulationCanvas } from './SimulationCanvas'; +export { TopBar } from './TopBar'; +export { ReportBuilderShell } from './ReportBuilderShell'; diff --git a/app/src/pages/reportBuilder/components/shared/CountryMapIcon.tsx b/app/src/pages/reportBuilder/components/shared/CountryMapIcon.tsx new file mode 100644 index 000000000..7f849c165 --- /dev/null +++ b/app/src/pages/reportBuilder/components/shared/CountryMapIcon.tsx @@ -0,0 +1,14 @@ +import { UKOutlineIcon, USOutlineIcon } from '@/components/icons/CountryOutlineIcons'; + +interface CountryMapIconProps { + countryId: string; + size: number; + color: string; +} + +export function CountryMapIcon({ countryId, size, color }: CountryMapIconProps) { + if (countryId === 'uk') { + return ; + } + return ; +} diff --git a/app/src/pages/reportBuilder/components/shared/CreationStatusHeader.tsx b/app/src/pages/reportBuilder/components/shared/CreationStatusHeader.tsx new file mode 100644 index 000000000..57446d9d1 --- /dev/null +++ b/app/src/pages/reportBuilder/components/shared/CreationStatusHeader.tsx @@ -0,0 +1,152 @@ +import { IconPencil } from '@tabler/icons-react'; +import { ActionIcon, Box, Group, Text, TextInput } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { CreationStatusHeaderProps } from '../../types'; + +/** + * A reusable glassmorphic status header for creation modes. + * Used in both PolicyBrowseModal and PopulationBrowseModal when in creation mode. + */ +export function CreationStatusHeader({ + colorConfig, + icon, + label, + placeholder, + isEditingLabel, + onLabelChange, + onStartEditing, + onStopEditing, + statusText, + hasChanges, + children, +}: CreationStatusHeaderProps) { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === 'Escape') { + onStopEditing(); + } + }; + + return ( + + + {/* Left side: Icon and editable name */} + + {/* Icon container */} + + {icon} + + + {/* Editable name */} + + {isEditingLabel ? ( + onLabelChange(e.currentTarget.value)} + onBlur={onStopEditing} + onKeyDown={handleKeyDown} + autoFocus + placeholder={placeholder.replace('Click to ', 'Enter ')} + size="xs" + style={{ width: 250 }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + border: 'none', + background: 'transparent', + padding: 0, + }, + }} + /> + ) : ( + <> + + {label || placeholder} + + + + + + )} + + + + {/* Right side: Status indicator */} + + {/* Status with indicator dot */} + + {hasChanges ? ( + <> + + + {statusText} + + + ) : ( + + {statusText} + + )} + + + {/* Optional children for additional content */} + {children} + + + + ); +} diff --git a/app/src/pages/reportBuilder/components/shared/ProgressDot.tsx b/app/src/pages/reportBuilder/components/shared/ProgressDot.tsx new file mode 100644 index 000000000..92fb5ed26 --- /dev/null +++ b/app/src/pages/reportBuilder/components/shared/ProgressDot.tsx @@ -0,0 +1,22 @@ +import { Box } from '@mantine/core'; +import { colors } from '@/designTokens'; + +interface ProgressDotProps { + filled: boolean; + pulsing?: boolean; +} + +export function ProgressDot({ filled, pulsing }: ProgressDotProps) { + return ( + + ); +} diff --git a/app/src/pages/reportBuilder/components/shared/index.ts b/app/src/pages/reportBuilder/components/shared/index.ts new file mode 100644 index 000000000..5d902db6b --- /dev/null +++ b/app/src/pages/reportBuilder/components/shared/index.ts @@ -0,0 +1,3 @@ +export { ProgressDot } from './ProgressDot'; +export { CountryMapIcon } from './CountryMapIcon'; +export { CreationStatusHeader } from './CreationStatusHeader'; diff --git a/app/src/pages/reportBuilder/constants.ts b/app/src/pages/reportBuilder/constants.ts new file mode 100644 index 000000000..a7d8a824e --- /dev/null +++ b/app/src/pages/reportBuilder/constants.ts @@ -0,0 +1,111 @@ +/** + * Constants for ReportBuilder components + */ +import { colors, typography } from '@/designTokens'; +import { IngredientColorConfig } from './types'; + +// ============================================================================ +// FONT SIZES +// ============================================================================ + +export const FONT_SIZES = { + title: typography.fontSize['3xl'], // 28px + normal: typography.fontSize.base, // 16px + small: typography.fontSize.sm, // 14px + tiny: typography.fontSize.xs, // 12px +}; + +// ============================================================================ +// INGREDIENT COLORS +// ============================================================================ + +export const INGREDIENT_COLORS: Record< + 'policy' | 'population' | 'dynamics', + IngredientColorConfig +> = { + policy: { + icon: colors.primary[600], + bg: colors.primary[50], + border: colors.primary[200], + accent: colors.primary[500], + }, + population: { + icon: colors.secondary[600], + bg: colors.secondary[50], + border: colors.secondary[200], + accent: colors.secondary[500], + }, + dynamics: { + // Muted gray-green for dynamics (distinct from teal and slate) + icon: colors.gray[500], + bg: colors.gray[50], + border: colors.gray[200], + accent: colors.gray[400], + }, +}; + +// ============================================================================ +// COUNTRY CONFIGURATION +// ============================================================================ + +export const COUNTRY_CONFIG = { + us: { + nationwideTitle: 'United States', + nationwideSubtitle: 'Nationwide', + nationwideLabel: 'United States', + geographyId: 'us', + }, + uk: { + nationwideTitle: 'United Kingdom', + nationwideSubtitle: 'UK-wide', + nationwideLabel: 'United Kingdom', + geographyId: 'uk', + }, +} as const; + +// ============================================================================ +// SAMPLE POPULATIONS +// ============================================================================ + +export const getSamplePopulations = (countryId: 'us' | 'uk') => { + const config = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + return { + household: { + label: 'Smith family (4 members)', + type: 'household' as const, + household: { + id: 'sample-household', + countryId, + householdData: { people: { person1: { age: { 2025: 40 } } } }, + }, + geography: null, + }, + nationwide: { + label: config.nationwideLabel, + type: 'geography' as const, + household: null, + geography: { + id: config.geographyId, + countryId, + scope: 'national' as const, + geographyId: config.geographyId, + name: config.nationwideLabel, + }, + }, + }; +}; + +// Default sample populations (for backwards compatibility) +export const SAMPLE_POPULATIONS = getSamplePopulations('us'); + +// ============================================================================ +// MODAL CONFIGURATION +// ============================================================================ + +export const BROWSE_MODAL_CONFIG = { + width: '90vw', + maxWidth: '1400px', + height: '92vh', + maxHeight: '1300px', + sidebarWidth: 220, +}; diff --git a/app/src/pages/reportBuilder/currentLaw.ts b/app/src/pages/reportBuilder/currentLaw.ts new file mode 100644 index 000000000..6df356ce2 --- /dev/null +++ b/app/src/pages/reportBuilder/currentLaw.ts @@ -0,0 +1,21 @@ +import type { PolicyStateProps } from '@/types/pathwayState'; + +export const CURRENT_LAW_ID = 'current-law' as const; + +export const CURRENT_LAW_LABEL = 'Current law'; + +export function isCurrentLaw(policyId: string | undefined): boolean { + return policyId === CURRENT_LAW_ID; +} + +export function createCurrentLawPolicy(): PolicyStateProps { + return { id: CURRENT_LAW_ID, label: CURRENT_LAW_LABEL, parameters: [] }; +} + +export function toApiPolicyId(localId: string, currentLawId: number): string { + return localId === CURRENT_LAW_ID ? currentLawId.toString() : localId; +} + +export function fromApiPolicyId(apiId: string, currentLawId: number): string { + return apiId === currentLawId.toString() ? CURRENT_LAW_ID : apiId; +} diff --git a/app/src/pages/reportBuilder/hooks/useModifyReportSubmission.ts b/app/src/pages/reportBuilder/hooks/useModifyReportSubmission.ts new file mode 100644 index 000000000..3b427da67 --- /dev/null +++ b/app/src/pages/reportBuilder/hooks/useModifyReportSubmission.ts @@ -0,0 +1,270 @@ +/** + * useModifyReportSubmission - Submission logic for the modify report page + * + * Supports two modes: + * - "Save as new report": Creates new base ingredients + new UserReport association + * - "Replace existing report": Creates new base ingredients + updates existing UserReport + * to point to the new base report + * + * In both cases, base ingredients (Simulation, Report) are always freshly created + * via the API — only the user association layer differs. + */ +import { useCallback, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; +import { ReportAdapter, SimulationAdapter } from '@/adapters'; +import { createReport as createBaseReport, createReportAndAssociateWithUser } from '@/api/report'; +import { createSimulation } from '@/api/simulation'; +import { MOCK_USER_ID } from '@/constants'; +import { useCalcOrchestratorManager } from '@/contexts/CalcOrchestratorContext'; +import { useUpdateReportAssociation } from '@/hooks/useUserReportAssociations'; +import { reportAssociationKeys, reportKeys } from '@/libs/queryKeys'; +import { RootState } from '@/store'; +import { Report } from '@/types/ingredients/Report'; +import { Simulation } from '@/types/ingredients/Simulation'; +import { toApiPolicyId } from '../currentLaw'; +import { ReportBuilderState } from '../types'; + +interface UseModifyReportSubmissionArgs { + reportState: ReportBuilderState; + countryId: 'us' | 'uk'; + existingUserReportId: string; + onSuccess: (userReportId: string) => void; +} + +interface UseModifyReportSubmissionReturn { + handleSaveAsNew: (label: string) => Promise; + handleReplace: () => Promise; + isSavingNew: boolean; + isReplacing: boolean; +} + +export function useModifyReportSubmission({ + reportState, + countryId, + existingUserReportId, + onSuccess, +}: UseModifyReportSubmissionArgs): UseModifyReportSubmissionReturn { + const currentLawId = useSelector((state: RootState) => state.metadata.currentLawId); + const manager = useCalcOrchestratorManager(); + const updateReportAssociation = useUpdateReportAssociation(); + const queryClient = useQueryClient(); + const [isSavingNew, setIsSavingNew] = useState(false); + const [isReplacing, setIsReplacing] = useState(false); + + /** + * Shared logic: create simulations via API and build the report payload. + * Returns the created simulation IDs, domain-model simulations, and report payload. + */ + const createSimulationsAndReport = useCallback(async () => { + const simulationIds: string[] = []; + const simulations: (Simulation | null)[] = []; + + for (const simState of reportState.simulations) { + const policyId = simState.policy?.id + ? toApiPolicyId(simState.policy.id, currentLawId) + : undefined; + + if (!policyId) { + console.error('[useModifyReportSubmission] Simulation missing policy ID'); + continue; + } + + let populationId: string | undefined; + let populationType: 'household' | 'geography' | undefined; + + if (simState.population?.household?.id) { + populationId = simState.population.household.id; + populationType = 'household'; + } else if (simState.population?.geography?.geographyId) { + populationId = simState.population.geography.geographyId; + populationType = 'geography'; + } + + if (!populationId || !populationType) { + console.error('[useModifyReportSubmission] Simulation missing population'); + continue; + } + + const payload = SimulationAdapter.toCreationPayload({ + populationId, + policyId, + populationType, + }); + const result = await createSimulation(countryId, payload); + const simulationId = result.result.simulation_id; + simulationIds.push(simulationId); + + simulations.push({ + id: simulationId, + countryId, + apiVersion: undefined, + policyId, + populationId, + populationType, + label: simState.label, + isCreated: true, + output: null, + status: 'pending', + }); + } + + if (simulationIds.length === 0) { + throw new Error('No simulations created'); + } + + const reportPayload = ReportAdapter.toCreationPayload({ + countryId, + year: reportState.year, + simulationIds, + apiVersion: null, + } as Report); + + return { simulationIds, simulations, reportPayload }; + }, [reportState, countryId, currentLawId]); + + /** + * Shared logic: start calculation after a report is created. + * Mirrors the logic in useCreateReport.onSuccess. + */ + const startCalculation = useCallback( + async (report: Report, simulations: (Simulation | null)[]) => { + const simulation1 = simulations[0]; + if (!simulation1) { + return; + } + + const isHouseholdReport = simulation1.populationType === 'household'; + + if (isHouseholdReport) { + const allSims = simulations.filter((s): s is Simulation => s !== null && s?.id != null); + for (const sim of allSims) { + manager + .startCalculation({ + calcId: sim.id!, + targetType: 'simulation', + countryId: report.countryId, + year: report.year, + simulations: { simulation1: sim, simulation2: null }, + populations: { + household1: reportState.simulations[0]?.population?.household || null, + household2: null, + geography1: null, + geography2: null, + }, + }) + .catch((err) => { + console.error( + `[useModifyReportSubmission] Calculation failed for sim ${sim.id}:`, + err + ); + }); + } + } else { + await manager.startCalculation({ + calcId: String(report.id), + targetType: 'report', + countryId: report.countryId, + year: report.year, + simulations: { + simulation1, + simulation2: simulations[1] || null, + }, + populations: { + household1: null, + household2: null, + geography1: reportState.simulations[0]?.population?.geography || null, + geography2: null, + }, + }); + } + }, + [manager, reportState] + ); + + /** + * Save as new report: create new base report + new UserReport association. + * Accepts a label for the new report. + */ + const handleSaveAsNew = useCallback( + async (label: string) => { + if (isSavingNew || isReplacing) { + return; + } + setIsSavingNew(true); + + try { + const { simulations, reportPayload } = await createSimulationsAndReport(); + + const result = await createReportAndAssociateWithUser({ + countryId, + payload: reportPayload, + userId: MOCK_USER_ID, + label: label || undefined, + }); + + queryClient.invalidateQueries({ queryKey: reportKeys.all }); + queryClient.invalidateQueries({ queryKey: reportAssociationKeys.all }); + + await startCalculation(result.report, simulations); + onSuccess(result.userReport.id); + } catch (error) { + console.error('[useModifyReportSubmission] Save as new failed:', error); + setIsSavingNew(false); + } + }, + [ + isSavingNew, + isReplacing, + createSimulationsAndReport, + countryId, + queryClient, + startCalculation, + onSuccess, + ] + ); + + /** + * Replace existing report: create new base report, then update the + * existing UserReport association to point to the new base report ID. + */ + const handleReplace = useCallback(async () => { + if (isSavingNew || isReplacing) { + return; + } + setIsReplacing(true); + + try { + const { simulations, reportPayload } = await createSimulationsAndReport(); + + const reportMetadata = await createBaseReport(countryId, reportPayload); + const report = ReportAdapter.fromMetadata(reportMetadata); + + await updateReportAssociation.mutateAsync({ + userReportId: existingUserReportId, + updates: { reportId: String(report.id) }, + }); + + queryClient.invalidateQueries({ queryKey: reportKeys.all }); + queryClient.invalidateQueries({ queryKey: reportAssociationKeys.all }); + + await startCalculation(report, simulations); + onSuccess(existingUserReportId); + } catch (error) { + console.error('[useModifyReportSubmission] Replace failed:', error); + setIsReplacing(false); + } + }, [ + isSavingNew, + isReplacing, + createSimulationsAndReport, + countryId, + existingUserReportId, + updateReportAssociation, + queryClient, + startCalculation, + onSuccess, + ]); + + return { handleSaveAsNew, handleReplace, isSavingNew, isReplacing }; +} diff --git a/app/src/pages/reportBuilder/hooks/useReportBuilderState.ts b/app/src/pages/reportBuilder/hooks/useReportBuilderState.ts new file mode 100644 index 000000000..72af9e55d --- /dev/null +++ b/app/src/pages/reportBuilder/hooks/useReportBuilderState.ts @@ -0,0 +1,72 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useUserReportById } from '@/hooks/useUserReports'; +import { RootState } from '@/store'; +import type { ReportBuilderState } from '../types'; +import { hydrateReportBuilderState } from '../utils/hydrateReportBuilderState'; + +interface UseReportBuilderStateReturn { + reportState: ReportBuilderState | null; + setReportState: React.Dispatch>; + originalState: ReportBuilderState | null; + isLoading: boolean; + error: Error | null; +} + +export function useReportBuilderState(userReportId: string): UseReportBuilderStateReturn { + const currentLawId = useSelector((state: RootState) => state.metadata.currentLawId); + const data = useUserReportById(userReportId); + + const [reportState, setReportState] = useState(null); + const originalStateRef = useRef(null); + + useEffect(() => { + if ( + !data.isLoading && + !data.error && + data.userReport && + data.report && + data.simulations.length > 0 && + reportState === null + ) { + const hydrated = hydrateReportBuilderState({ + userReport: data.userReport, + report: data.report, + simulations: data.simulations, + policies: data.policies, + households: data.households, + geographies: data.geographies, + userSimulations: data.userSimulations, + userPolicies: data.userPolicies, + userHouseholds: data.userHouseholds, + userGeographies: data.userGeographies, + currentLawId, + }); + setReportState(hydrated); + originalStateRef.current = hydrated; + } + }, [ + data.isLoading, + data.error, + data.userReport, + data.report, + data.simulations, + data.policies, + data.households, + data.geographies, + data.userSimulations, + data.userPolicies, + data.userHouseholds, + data.userGeographies, + currentLawId, + reportState, + ]); + + return { + reportState, + setReportState, + originalState: originalStateRef.current, + isLoading: data.isLoading, + error: data.error, + }; +} diff --git a/app/src/pages/reportBuilder/hooks/useReportSubmission.ts b/app/src/pages/reportBuilder/hooks/useReportSubmission.ts new file mode 100644 index 000000000..91136ad00 --- /dev/null +++ b/app/src/pages/reportBuilder/hooks/useReportSubmission.ts @@ -0,0 +1,197 @@ +/** + * useReportSubmission - Extracted submission logic for creating a new report + * + * Handles: + * - Sequential simulation creation via API + * - Report creation with simulation IDs + * - isReportConfigured derivation + * - isSubmitting state + * + * Accepts an onSuccess callback instead of navigating directly, + * so the consuming page controls routing. + */ +import { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { ReportAdapter, SimulationAdapter } from '@/adapters'; +import { createSimulation } from '@/api/simulation'; +import { useCreateReport } from '@/hooks/useCreateReport'; +import { RootState } from '@/store'; +import { Report } from '@/types/ingredients/Report'; +import { Simulation } from '@/types/ingredients/Simulation'; +import { SimulationStateProps } from '@/types/pathwayState'; +import { toApiPolicyId } from '../currentLaw'; +import { ReportBuilderState } from '../types'; + +interface UseReportSubmissionArgs { + reportState: ReportBuilderState; + countryId: 'us' | 'uk'; + onSuccess: (userReportId: string) => void; +} + +interface UseReportSubmissionReturn { + handleSubmit: () => Promise; + isSubmitting: boolean; + isReportConfigured: boolean; +} + +function convertToSimulation( + simState: SimulationStateProps, + simulationId: string, + countryId: 'us' | 'uk', + currentLawId: number +): Simulation | null { + const policyId = simState.policy?.id; + if (!policyId) { + return null; + } + + let populationId: string | undefined; + let populationType: 'household' | 'geography' | undefined; + + if (simState.population?.household?.id) { + populationId = simState.population.household.id; + populationType = 'household'; + } else if (simState.population?.geography?.geographyId) { + populationId = simState.population.geography.geographyId; + populationType = 'geography'; + } + + if (!populationId || !populationType) { + return null; + } + + return { + id: simulationId, + countryId, + apiVersion: undefined, + policyId: toApiPolicyId(policyId, currentLawId), + populationId, + populationType, + label: simState.label, + isCreated: true, + output: null, + status: 'pending', + }; +} + +export function useReportSubmission({ + reportState, + countryId, + onSuccess, +}: UseReportSubmissionArgs): UseReportSubmissionReturn { + const currentLawId = useSelector((state: RootState) => state.metadata.currentLawId); + const [isSubmitting, setIsSubmitting] = useState(false); + const { createReport } = useCreateReport(reportState.label || undefined); + + const isReportConfigured = reportState.simulations.every( + (sim) => !!sim.policy.id && !!(sim.population.household?.id || sim.population.geography?.id) + ); + + const handleSubmit = useCallback(async () => { + if (!isReportConfigured || isSubmitting) { + return; + } + + setIsSubmitting(true); + + try { + const simulationIds: string[] = []; + const simulations: (Simulation | null)[] = []; + + for (const simState of reportState.simulations) { + const policyId = simState.policy?.id + ? toApiPolicyId(simState.policy.id, currentLawId) + : undefined; + + if (!policyId) { + console.error('[useReportSubmission] Simulation missing policy ID'); + continue; + } + + let populationId: string | undefined; + let populationType: 'household' | 'geography' | undefined; + + if (simState.population?.household?.id) { + populationId = simState.population.household.id; + populationType = 'household'; + } else if (simState.population?.geography?.geographyId) { + populationId = simState.population.geography.geographyId; + populationType = 'geography'; + } + + if (!populationId || !populationType) { + console.error('[useReportSubmission] Simulation missing population'); + continue; + } + + const simulationData: Partial = { + populationId, + policyId, + populationType, + }; + + const payload = SimulationAdapter.toCreationPayload(simulationData); + const result = await createSimulation(countryId, payload); + const simulationId = result.result.simulation_id; + simulationIds.push(simulationId); + + const simulation = convertToSimulation(simState, simulationId, countryId, currentLawId); + simulations.push(simulation); + } + + if (simulationIds.length === 0) { + console.error('[useReportSubmission] No simulations created'); + setIsSubmitting(false); + return; + } + + const reportData: Partial = { + countryId, + year: reportState.year, + simulationIds, + apiVersion: null, + }; + + const serializedPayload = ReportAdapter.toCreationPayload(reportData as Report); + + await createReport( + { + countryId, + payload: serializedPayload, + simulations: { + simulation1: simulations[0], + simulation2: simulations[1] || null, + }, + populations: { + household1: reportState.simulations[0]?.population?.household || null, + household2: reportState.simulations[1]?.population?.household || null, + geography1: reportState.simulations[0]?.population?.geography || null, + geography2: reportState.simulations[1]?.population?.geography || null, + }, + }, + { + onSuccess: (data) => { + onSuccess(data.userReport.id); + }, + onError: (error) => { + console.error('[useReportSubmission] Report creation failed:', error); + setIsSubmitting(false); + }, + } + ); + } catch (error) { + console.error('[useReportSubmission] Error running report:', error); + setIsSubmitting(false); + } + }, [ + isReportConfigured, + isSubmitting, + reportState, + countryId, + currentLawId, + createReport, + onSuccess, + ]); + + return { handleSubmit, isSubmitting, isReportConfigured }; +} diff --git a/app/src/pages/reportBuilder/hooks/useSimulationCanvas.ts b/app/src/pages/reportBuilder/hooks/useSimulationCanvas.ts new file mode 100644 index 000000000..5e345be52 --- /dev/null +++ b/app/src/pages/reportBuilder/hooks/useSimulationCanvas.ts @@ -0,0 +1,540 @@ +/** + * useSimulationCanvas - State management hook for the simulation canvas + * + * Owns: + * - Data fetching (policies, households, regions) and loading state + * - Computed/derived data (savedPolicies, recentPopulations) + * - Modal visibility state (policy browse, population browse, policy creation) + * - All simulation mutation callbacks (add, remove, select, deselect, etc.) + * + * The component that consumes this hook is responsible only for rendering. + */ + +import { useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { HouseholdAdapter } from '@/adapters/HouseholdAdapter'; +import { geographyUsageStore, householdUsageStore } from '@/api/usageTracking'; +import { MOCK_USER_ID } from '@/constants'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useUserHouseholds } from '@/hooks/useUserHousehold'; +import { useUserPolicies } from '@/hooks/useUserPolicy'; +import { RootState } from '@/store'; +import { Geography } from '@/types/ingredients/Geography'; +import { PolicyStateProps, PopulationStateProps } from '@/types/pathwayState'; +import { countPolicyModifications } from '@/utils/countParameterChanges'; +import { generateGeographyLabel } from '@/utils/geographyUtils'; +import { initializePolicyState } from '@/utils/pathwayState/initializePolicyState'; +import { initializePopulationState } from '@/utils/pathwayState/initializePopulationState'; +import { initializeSimulationState } from '@/utils/pathwayState/initializeSimulationState'; +import { + getUKConstituencies, + getUKCountries, + getUKLocalAuthorities, + getUSCongressionalDistricts, + getUSPlaces, + getUSStates, + RegionOption, +} from '@/utils/regionStrategies'; +import { getSamplePopulations } from '../constants'; +import { createCurrentLawPolicy } from '../currentLaw'; +import type { + IngredientPickerState, + IngredientType, + PolicyBrowseState, + RecentPopulation, + ReportBuilderState, + SavedPolicy, +} from '../types'; + +interface UseSimulationCanvasArgs { + reportState: ReportBuilderState; + setReportState: React.Dispatch>; + pickerState: IngredientPickerState; + setPickerState: React.Dispatch>; +} + +export function useSimulationCanvas({ + reportState, + setReportState, + pickerState, + setPickerState, +}: UseSimulationCanvasArgs) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const userId = MOCK_USER_ID.toString(); + const { data: policies, isLoading: policiesLoading } = useUserPolicies(userId); + const { data: households, isLoading: householdsLoading } = useUserHouseholds(userId); + const regionOptions = useSelector((state: RootState) => state.metadata.economyOptions.region); + const metadataLoading = useSelector((state: RootState) => state.metadata.loading); + const isGeographySelected = !!reportState.simulations[0]?.population?.geography?.id; + + // Show loading skeleton until all data sources have resolved. + // Region options check guards against a race condition where metadata initial + // state has loading=false but region=[] before the actual data arrives. + const isInitialLoading = + policiesLoading || + householdsLoading || + metadataLoading || + policies === undefined || + households === undefined || + regionOptions.length === 0; + + // --------------------------------------------------------------------------- + // Modal visibility state + // --------------------------------------------------------------------------- + + const [policyBrowseState, setPolicyBrowseState] = useState({ + isOpen: false, + simulationIndex: 0, + }); + + const [policyCreationState, setPolicyCreationState] = useState({ + isOpen: false, + simulationIndex: 0, + }); + + const [populationBrowseState, setPopulationBrowseState] = useState({ + isOpen: false, + simulationIndex: 0, + }); + + // --------------------------------------------------------------------------- + // Computed / derived data + // --------------------------------------------------------------------------- + + const savedPolicies: SavedPolicy[] = useMemo(() => { + return (policies || []) + .map((p) => { + const policyId = p.association.policyId.toString(); + return { + id: policyId, + label: p.association.label || `Policy #${policyId}`, + paramCount: countPolicyModifications(p.policy), + createdAt: p.association.createdAt, + updatedAt: p.association.updatedAt, + }; + }) + .sort((a, b) => { + const aTime = a.updatedAt || a.createdAt || ''; + const bTime = b.updatedAt || b.createdAt || ''; + return bTime.localeCompare(aTime); + }); + }, [policies]); + + const recentPopulations: RecentPopulation[] = useMemo(() => { + const results: Array = []; + + const regions = regionOptions || []; + const allRegions: RegionOption[] = + countryId === 'us' + ? [...getUSStates(regions), ...getUSCongressionalDistricts(regions), ...getUSPlaces()] + : [ + ...getUKCountries(regions), + ...getUKConstituencies(regions), + ...getUKLocalAuthorities(regions), + ]; + + for (const geoId of geographyUsageStore.getRecentIds(10)) { + if (geoId === 'us' || geoId === 'uk') { + continue; + } + + const region = allRegions.find((r) => r.value === geoId); + if (!region) { + continue; + } + + const geographyId = `${countryId}-${geoId}`; + const geography: Geography = { + id: geographyId, + countryId, + scope: 'subnational', + geographyId: geoId, + }; + results.push({ + id: geographyId, + label: region.label, + type: 'geography', + population: { + geography, + household: null, + label: generateGeographyLabel(geography), + type: 'geography', + }, + timestamp: geographyUsageStore.getLastUsed(geoId) || '', + }); + } + + for (const householdId of householdUsageStore.getRecentIds(10)) { + const householdData = households?.find( + (h) => String(h.association.householdId) === householdId + ); + if (!householdData?.household?.household_json) { + continue; + } + + const household = HouseholdAdapter.fromMetadata(householdData.household); + const resolvedId = household.id || householdId; + const label = householdData.association.label || `Household #${householdId}`; + results.push({ + id: resolvedId, + label, + type: 'household', + population: { + geography: null, + household, + label, + type: 'household', + }, + timestamp: householdUsageStore.getLastUsed(householdId) || '', + }); + } + + return results + .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) + .slice(0, 10) + .map(({ timestamp: _t, ...rest }) => rest); + }, [countryId, households, regionOptions]); + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + /** + * Update a simulation's population at `index`, and if it's the baseline (0) + * propagate the same population to sim[1] when it exists. + */ + const updatePopulationWithInheritance = useCallback( + (simulationIndex: number, population: PopulationStateProps) => { + setReportState((prev) => { + const newSimulations = prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, population: { ...population } } : sim + ); + + if (simulationIndex === 0 && newSimulations.length > 1) { + newSimulations[1] = { ...newSimulations[1], population: { ...population } }; + } + + return { ...prev, simulations: newSimulations }; + }); + }, + [setReportState] + ); + + /** Update a single simulation's policy at `index`. */ + const updatePolicy = useCallback( + (simulationIndex: number, policy: PolicyStateProps) => { + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => + i === simulationIndex ? { ...sim, policy } : sim + ), + })); + }, + [setReportState] + ); + + // --------------------------------------------------------------------------- + // Simulation-level actions + // --------------------------------------------------------------------------- + + const handleAddSimulation = useCallback(() => { + setReportState((prev) => { + if (prev.simulations.length >= 2) { + return prev; + } + const newSim = initializeSimulationState(); + newSim.label = 'Reform simulation'; + newSim.population = { ...prev.simulations[0].population }; + return { ...prev, simulations: [...prev.simulations, newSim] }; + }); + }, [setReportState]); + + const handleRemoveSimulation = useCallback( + (index: number) => { + if (index === 0) { + return; + } + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.filter((_, i) => i !== index), + })); + }, + [setReportState] + ); + + const handleSimulationLabelChange = useCallback( + (index: number, label: string) => { + setReportState((prev) => ({ + ...prev, + simulations: prev.simulations.map((sim, i) => (i === index ? { ...sim, label } : sim)), + })); + }, + [setReportState] + ); + + // --------------------------------------------------------------------------- + // Policy actions + // --------------------------------------------------------------------------- + + const handleQuickSelectPolicy = useCallback( + (simulationIndex: number) => { + updatePolicy(simulationIndex, createCurrentLawPolicy()); + }, + [updatePolicy] + ); + + const handleSelectSavedPolicy = useCallback( + (simulationIndex: number, policyId: string, label: string, paramCount: number) => { + updatePolicy(simulationIndex, { + id: policyId, + label, + parameters: Array(paramCount).fill({}), + }); + }, + [updatePolicy] + ); + + const handleDeselectPolicy = useCallback( + (simulationIndex: number) => { + updatePolicy(simulationIndex, initializePolicyState()); + }, + [updatePolicy] + ); + + const handleBrowseMorePolicies = useCallback((simulationIndex: number) => { + setPolicyBrowseState({ isOpen: true, simulationIndex }); + }, []); + + const handlePolicySelectFromBrowse = useCallback( + (policy: PolicyStateProps) => { + updatePolicy(policyBrowseState.simulationIndex, policy); + }, + [policyBrowseState.simulationIndex, updatePolicy] + ); + + const handlePolicyCreated = useCallback( + (simulationIndex: number, policy: PolicyStateProps) => { + updatePolicy(simulationIndex, { + id: policy.id, + label: policy.label, + parameters: policy.parameters, + }); + }, + [updatePolicy] + ); + + const handleEditPolicy = useCallback( + (simulationIndex: number) => { + const currentPolicy = reportState.simulations[simulationIndex]?.policy; + if (!currentPolicy?.id) { + return; + } + + // Resolve full parameters: in-session policies have real params, + // saved policies have placeholder Array(n).fill({}) + let resolvedPolicy = currentPolicy; + const hasRealParams = + currentPolicy.parameters.length > 0 && !!currentPolicy.parameters[0]?.name; + if (!hasRealParams && currentPolicy.id) { + const fullPolicy = policies?.find( + (p) => p.association.policyId.toString() === currentPolicy.id + ); + if (fullPolicy?.policy?.parameters) { + resolvedPolicy = { + ...currentPolicy, + parameters: fullPolicy.policy.parameters, + }; + } + } + + setPolicyCreationState({ + isOpen: true, + simulationIndex, + initialPolicy: resolvedPolicy, + }); + }, + [reportState.simulations, policies] + ); + + const handleViewPolicy = useCallback( + (simulationIndex: number) => { + const currentPolicy = reportState.simulations[simulationIndex]?.policy; + if (!currentPolicy?.id) { + return; + } + + // Resolve full parameters (same as handleEditPolicy) + let resolvedPolicy = currentPolicy; + const hasRealParams = + currentPolicy.parameters.length > 0 && !!currentPolicy.parameters[0]?.name; + if (!hasRealParams && currentPolicy.id) { + const fullPolicy = policies?.find( + (p) => p.association.policyId.toString() === currentPolicy.id + ); + if (fullPolicy?.policy?.parameters) { + resolvedPolicy = { + ...currentPolicy, + parameters: fullPolicy.policy.parameters, + }; + } + } + + setPolicyCreationState({ + isOpen: true, + simulationIndex, + initialPolicy: resolvedPolicy, + initialEditorMode: 'display', + }); + }, + [reportState.simulations, policies] + ); + + // --------------------------------------------------------------------------- + // Population actions + // --------------------------------------------------------------------------- + + const handleQuickSelectPopulation = useCallback( + (simulationIndex: number, _populationType: 'nationwide') => { + const populationState = getSamplePopulations(countryId).nationwide; + + if (populationState.geography?.geographyId) { + geographyUsageStore.recordUsage(populationState.geography.geographyId); + } + + updatePopulationWithInheritance(simulationIndex, populationState); + }, + [countryId, updatePopulationWithInheritance] + ); + + const handleSelectRecentPopulation = useCallback( + (simulationIndex: number, population: PopulationStateProps) => { + if (population.geography?.geographyId) { + geographyUsageStore.recordUsage(population.geography.geographyId); + } else if (population.household?.id) { + householdUsageStore.recordUsage(population.household.id); + } + + updatePopulationWithInheritance(simulationIndex, population); + }, + [updatePopulationWithInheritance] + ); + + const handleDeselectPopulation = useCallback( + (simulationIndex: number) => { + updatePopulationWithInheritance(simulationIndex, initializePopulationState()); + }, + [updatePopulationWithInheritance] + ); + + const handleBrowseMorePopulations = useCallback((simulationIndex: number) => { + setPopulationBrowseState({ isOpen: true, simulationIndex }); + }, []); + + const handlePopulationSelectFromBrowse = useCallback( + (population: PopulationStateProps) => { + updatePopulationWithInheritance(populationBrowseState.simulationIndex, population); + }, + [populationBrowseState.simulationIndex, updatePopulationWithInheritance] + ); + + // --------------------------------------------------------------------------- + // Ingredient picker / create-custom actions + // --------------------------------------------------------------------------- + + const handleIngredientSelect = useCallback( + (item: PolicyStateProps | PopulationStateProps | null) => { + const { simulationIndex, ingredientType } = pickerState; + if (ingredientType === 'policy') { + updatePolicy(simulationIndex, item as PolicyStateProps); + } else if (ingredientType === 'population') { + updatePopulationWithInheritance(simulationIndex, item as PopulationStateProps); + } + }, + [pickerState, updatePolicy, updatePopulationWithInheritance] + ); + + const handleCreateCustom = useCallback( + (simulationIndex: number, ingredientType: IngredientType) => { + if (ingredientType === 'policy') { + setPolicyCreationState({ isOpen: true, simulationIndex }); + } else if (ingredientType === 'population') { + window.location.href = `/${countryId}/households/create`; + } + }, + [countryId] + ); + + // --------------------------------------------------------------------------- + // Modal close helpers + // --------------------------------------------------------------------------- + + const closePolicyBrowse = useCallback( + () => setPolicyBrowseState((prev) => ({ ...prev, isOpen: false })), + [] + ); + + const closePolicyCreation = useCallback( + () => setPolicyCreationState((prev) => ({ ...prev, isOpen: false })), + [] + ); + + const closePopulationBrowse = useCallback( + () => setPopulationBrowseState((prev) => ({ ...prev, isOpen: false })), + [] + ); + + const closeIngredientPicker = useCallback( + () => setPickerState((prev) => ({ ...prev, isOpen: false })), + [setPickerState] + ); + + // --------------------------------------------------------------------------- + // Return + // --------------------------------------------------------------------------- + + return { + countryId, + isInitialLoading, + isGeographySelected, + + // Computed data + savedPolicies, + recentPopulations, + + // Simulation actions + handleAddSimulation, + handleRemoveSimulation, + handleSimulationLabelChange, + + // Policy actions + handleQuickSelectPolicy, + handleSelectSavedPolicy, + handleDeselectPolicy, + handleEditPolicy, + handleViewPolicy, + handleBrowseMorePolicies, + handlePolicySelectFromBrowse, + handlePolicyCreated, + + // Population actions + handleQuickSelectPopulation, + handleSelectRecentPopulation, + handleDeselectPopulation, + handleBrowseMorePopulations, + handlePopulationSelectFromBrowse, + + // Ingredient picker / custom + handleIngredientSelect, + handleCreateCustom, + + // Modal state + pickerState, + policyBrowseState, + policyCreationState, + populationBrowseState, + closePolicyBrowse, + closePolicyCreation, + closePopulationBrowse, + closeIngredientPicker, + }; +} diff --git a/app/src/pages/reportBuilder/index.ts b/app/src/pages/reportBuilder/index.ts new file mode 100644 index 000000000..1f2df775b --- /dev/null +++ b/app/src/pages/reportBuilder/index.ts @@ -0,0 +1,37 @@ +/** + * ReportBuilder Module + * + * This folder contains the refactored ReportBuilder page and its components. + * + * Structure: + * - types.ts: All TypeScript interfaces and types + * - constants.ts: Configuration constants (FONT_SIZES, INGREDIENT_COLORS, etc.) + * - styles.ts: Shared style objects + * - components/: Reusable UI components + * - chips/: Chip components (OptionChipSquare, OptionChipRow, etc.) + * - shared/: Shared components (CreationStatusHeader, ProgressDot, etc.) + * - IngredientSection, SimulationBlock, AddSimulationCard, etc. + * - modals/: Modal components + * - BrowseModalTemplate.tsx: Template for browse modals + * - PolicyBrowseModal.tsx: Policy browsing and creation + * - PopulationBrowseModal.tsx: Population browsing and creation + * - PolicyCreationModal.tsx: Policy creation form + */ + +// Main page component +export { default } from './ReportBuilderPage'; + +// Types +export * from './types'; + +// Constants +export * from './constants'; + +// Styles +export * from './styles'; + +// Components +export * from './components'; + +// Modals +export * from './modals'; diff --git a/app/src/pages/reportBuilder/modals/BrowseModalTemplate.tsx b/app/src/pages/reportBuilder/modals/BrowseModalTemplate.tsx new file mode 100644 index 000000000..5e6b1d009 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/BrowseModalTemplate.tsx @@ -0,0 +1,242 @@ +/** + * BrowseModalTemplate - Shared template for browse modals (Policy, Population) + * + * Handles ONLY visual layout: + * - Modal shell with header + * - Sidebar (standard sections OR custom render) + * - Main content area + * - Optional status header slot + * - Optional footer slot + */ +import { IconChevronLeft } from '@tabler/icons-react'; +import { + Box, + Button, + Divider, + Group, + Modal, + ScrollArea, + Stack, + Text, + UnstyledButton, +} from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { BROWSE_MODAL_CONFIG, FONT_SIZES } from '../constants'; +import { modalStyles } from '../styles'; +import { BrowseModalSidebarSection, BrowseModalTemplateProps } from '../types'; + +/** + * A reusable template for browse modals (PolicyBrowseModal, PopulationBrowseModal). + * Handles only visual display logic - sidebar layout, modal chrome, content slots. + */ +export function BrowseModalTemplate({ + isOpen, + onClose, + headerIcon, + headerTitle, + headerSubtitle, + headerRightContent, + colorConfig, + sidebarSections, + renderSidebar, + sidebarWidth = BROWSE_MODAL_CONFIG.sidebarWidth, + renderMainContent, + statusHeader, + footer, + contentPadding = spacing.lg, +}: BrowseModalTemplateProps) { + // Render standard sidebar sections + const renderStandardSidebar = (sections: BrowseModalSidebarSection[]) => ( + + + {sections.map((section, sectionIndex) => ( + + {sectionIndex > 0 && } + + {section.label} + {section.items?.map((item) => ( + + {item.icon} + {item.label} + {item.badge !== undefined && ( + + {item.badge} + + )} + + ))} + + + ))} + + + ); + + return ( + + + + {headerIcon} + + + {typeof headerTitle === 'string' ? ( + + {headerTitle} + + ) : ( + headerTitle + )} + {headerSubtitle && ( + + {headerSubtitle} + + )} + + + {headerRightContent} + + } + size={BROWSE_MODAL_CONFIG.width} + radius="lg" + styles={{ + content: { + maxWidth: BROWSE_MODAL_CONFIG.maxWidth, + height: BROWSE_MODAL_CONFIG.height, + maxHeight: BROWSE_MODAL_CONFIG.maxHeight, + display: 'flex', + flexDirection: 'column', + }, + header: { + borderBottom: `1px solid ${colors.border.light}`, + paddingBottom: spacing.md, + paddingLeft: spacing.xl, + paddingRight: spacing.xl, + }, + title: { + flex: 1, + }, + body: { + padding: 0, + flex: 1, + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }, + }} + > + {/* Main layout: sidebar + content */} + + {/* Sidebar */} + + {renderSidebar + ? renderSidebar() + : sidebarSections && renderStandardSidebar(sidebarSections)} + + + {/* Main Content Area */} + + {/* Optional Status Header */} + {statusHeader} + + {/* Main Content */} + + {renderMainContent()} + + + + + {/* Footer spans full width, outside the sidebar/content layout */} + {footer && ( + + {footer} + + )} + + ); +} + +// ============================================================================ +// HELPER COMPONENTS +// ============================================================================ + +interface CreationModeFooterProps { + onBack: () => void; + onSubmit: () => void; + isLoading: boolean; + submitDisabled: boolean; + submitLabel: string; + backLabel?: string; +} + +export function CreationModeFooter({ + onBack, + onSubmit, + isLoading, + submitDisabled, + submitLabel, + backLabel = 'Back', +}: CreationModeFooterProps) { + return ( + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/IngredientPickerModal.tsx b/app/src/pages/reportBuilder/modals/IngredientPickerModal.tsx new file mode 100644 index 000000000..2b40e9a45 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/IngredientPickerModal.tsx @@ -0,0 +1,628 @@ +import { Fragment, useState } from 'react'; +import { + IconChartLine, + IconChevronRight, + IconHome, + IconInfoCircle, + IconPlus, + IconScale, + IconSparkles, + IconUsers, +} from '@tabler/icons-react'; +import { useSelector } from 'react-redux'; +import { + ActionIcon, + Box, + Button, + Divider, + Group, + Loader, + Modal, + Paper, + ScrollArea, + Stack, + Text, + Tooltip, +} from '@mantine/core'; +import { MOCK_USER_ID } from '@/constants'; +import { colors, spacing } from '@/designTokens'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useUserHouseholds } from '@/hooks/useUserHousehold'; +import { useUserPolicies } from '@/hooks/useUserPolicy'; +import { RootState } from '@/store'; +import { PolicyStateProps, PopulationStateProps } from '@/types/pathwayState'; +import { countPolicyModifications } from '@/utils/countParameterChanges'; +import { formatPeriod } from '@/utils/dateUtils'; +import { formatLabelParts, getHierarchicalLabels } from '@/utils/parameterLabels'; +import { formatParameterValue } from '@/utils/policyTableHelpers'; +import { CountryMapIcon } from '../components/shared/CountryMapIcon'; +import { COUNTRY_CONFIG, FONT_SIZES, INGREDIENT_COLORS } from '../constants'; +import { createCurrentLawPolicy } from '../currentLaw'; +import { IngredientType } from '../types'; + +interface IngredientPickerModalProps { + isOpen: boolean; + onClose: () => void; + type: IngredientType; + onSelect: (item: PolicyStateProps | PopulationStateProps | null) => void; + onCreateNew: () => void; +} + +export function IngredientPickerModal({ + isOpen, + onClose, + type, + onSelect, + onCreateNew, +}: IngredientPickerModalProps) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const countryConfig = COUNTRY_CONFIG[countryId] || COUNTRY_CONFIG.us; + const userId = MOCK_USER_ID.toString(); + const { data: policies, isLoading: policiesLoading } = useUserPolicies(userId); + const { data: households, isLoading: householdsLoading } = useUserHouseholds(userId); + const colorConfig = INGREDIENT_COLORS[type]; + const [expandedPolicyId, setExpandedPolicyId] = useState(null); + const parameters = useSelector((state: RootState) => state.metadata.parameters); + + const getTitle = () => { + switch (type) { + case 'policy': + return 'Select policy'; + case 'population': + return 'Select population'; + case 'dynamics': + return 'Configure dynamics'; + } + }; + + const getIcon = () => { + const iconProps = { size: 20, color: colorConfig.icon }; + switch (type) { + case 'policy': + return ; + case 'population': + return ; + case 'dynamics': + return ; + } + }; + + const handleSelectPolicy = (policyId: string, label: string, paramCount: number) => { + onSelect({ id: policyId, label, parameters: Array(paramCount).fill({}) }); + onClose(); + }; + + const handleSelectCurrentLaw = () => { + onSelect(createCurrentLawPolicy()); + onClose(); + }; + + const handleSelectHousehold = (householdId: string, label: string) => { + onSelect({ + label, + type: 'household', + household: { id: householdId, countryId, householdData: { people: {} } }, + geography: null, + }); + onClose(); + }; + + const handleSelectGeography = ( + geoId: string, + label: string, + scope: 'national' | 'subnational' + ) => { + onSelect({ + label, + type: 'geography', + household: null, + geography: { id: geoId, countryId, scope, geographyId: geoId, name: label }, + }); + onClose(); + }; + + return ( + + + {getIcon()} + + + {getTitle()} + + + } + size="xl" + radius="lg" + styles={{ + content: { width: '80vw', maxWidth: '1200px' }, + header: { borderBottom: `1px solid ${colors.border.light}`, paddingBottom: spacing.md }, + body: { padding: spacing.xl }, + }} + > + + {type === 'policy' && ( + <> + + + + + + + + Current law + + + Use existing tax and benefit rules without modifications + + + + + + + {policiesLoading ? ( + + + + ) : ( + + {policies?.map((p) => { + // Use association data for display (like Policies page) + const policyId = p.association.policyId.toString(); + const label = p.association.label || `Policy #${policyId}`; + const paramCount = countPolicyModifications(p.policy); // Handles undefined gracefully + const policyParams = p.policy?.parameters || []; + const isExpanded = expandedPolicyId === policyId; + + return ( + + {/* Main clickable row */} + handleSelectPolicy(policyId, label, paramCount)} + onMouseEnter={(e) => { + e.currentTarget.style.background = colors.gray[50]; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent'; + }} + > + {/* Policy info - takes remaining space */} + + + {label} + + + {paramCount} param{paramCount !== 1 ? 's' : ''} changed + + + + {/* Info/expand button - isolated click zone */} + { + e.stopPropagation(); // Prevent selection + setExpandedPolicyId(isExpanded ? null : policyId); + }} + style={{ marginRight: spacing.sm }} + aria-label={ + isExpanded ? 'Hide parameter details' : 'Show parameter details' + } + > + + + + {/* Select indicator */} + + + + {/* Expandable parameter details - table-like display */} + + {/* Unified grid for header and data rows */} + + {/* Header row */} + + Parameter + + + Changes + + + {/* Data rows - grouped by parameter */} + {(() => { + // Build grouped list of parameters with their changes + const groupedParams: Array<{ + paramName: string; + label: string; + changes: Array<{ period: string; value: string }>; + }> = []; + + policyParams.forEach((param) => { + const paramName = param.name; + const hierarchicalLabels = getHierarchicalLabels( + paramName, + parameters + ); + const displayLabel = + hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : paramName.split('.').pop() || paramName; + const metadata = parameters[paramName]; + + // Use value intervals directly from the Policy type + const changes = (param.values || []).map((interval) => ({ + period: formatPeriod(interval.startDate, interval.endDate), + value: formatParameterValue(interval.value, metadata?.unit), + })); + + groupedParams.push({ paramName, label: displayLabel, changes }); + }); + + if (groupedParams.length === 0) { + return ( + <> + + No parameter details available + + + ); + } + + const displayParams = groupedParams.slice(0, 10); + const remainingCount = groupedParams.length - 10; + + return ( + <> + {displayParams.map((param) => ( + + {/* Parameter name cell */} + + + + {param.label} + + + + {/* Changes cell - multiple lines */} + + {param.changes.map((change, idx) => ( + + + {change.period}: + {' '} + + {change.value} + + + ))} + + + ))} + {remainingCount > 0 && ( + + +{remainingCount} more parameter + {remainingCount !== 1 ? 's' : ''} + + )} + + ); + })()} + + + + ); + })} + {(!policies || policies.length === 0) && ( + + No saved policies + + )} + + )} + + + + + )} + + {type === 'population' && ( + <> + + handleSelectGeography( + countryConfig.geographyId, + countryConfig.nationwideLabel, + 'national' + ) + } + > + + + + + + + {countryConfig.nationwideTitle} + + + {countryConfig.nationwideSubtitle} + + + + + handleSelectHousehold('sample-household', 'Sample household')} + > + + + + + + + Sample household + + + Single household simulation + + + + + + + {householdsLoading ? ( + + + + ) : ( + + {households?.map((h) => { + // Use association data for display (like Populations page) + const householdId = h.association.householdId.toString(); + const label = h.association.label || `Household #${householdId}`; + return ( + handleSelectHousehold(householdId, label)} + > + + + {label} + + + + + ); + })} + {(!households || households.length === 0) && ( + + No saved households + + )} + + )} + + + + + )} + + {type === 'dynamics' && ( + + + + + + + Dynamics coming soon + + + Dynamic behavioral responses will be available in a future update. + + + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/PolicyBrowseModal.tsx b/app/src/pages/reportBuilder/modals/PolicyBrowseModal.tsx new file mode 100644 index 000000000..fb269017d --- /dev/null +++ b/app/src/pages/reportBuilder/modals/PolicyBrowseModal.tsx @@ -0,0 +1,777 @@ +/** + * PolicyBrowseModal - Full-featured policy browsing and creation modal + * + * Uses BrowseModalTemplate for visual layout and delegates to sub-components: + * - Browse mode: PolicyBrowseContent for main content + * - Creation mode: PolicyCreationContent + PolicyParameterTree + */ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { IconChevronRight, IconFolder, IconPlus, IconScale, IconStar } from '@tabler/icons-react'; +import { useSelector } from 'react-redux'; +import { Box, Button, Group, Modal, Paper, Stack, Text } from '@mantine/core'; +import { PolicyAdapter } from '@/adapters'; +import { createPolicy as createPolicyApi } from '@/api/policy'; +import { + EditAndSaveNewButton, + EditAndUpdateButton, + EditDefaultButton, +} from '@/components/common/ActionButtons'; +import { MOCK_USER_ID } from '@/constants'; +import { colors, spacing } from '@/designTokens'; +import { useCreatePolicy } from '@/hooks/useCreatePolicy'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useUpdatePolicyAssociation, useUserPolicies } from '@/hooks/useUserPolicy'; +import { getDateRange } from '@/libs/metadataUtils'; +import { ValueSetterMode } from '@/pathways/report/components/valueSetters'; +import { RootState } from '@/store'; +import { Policy } from '@/types/ingredients/Policy'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { PolicyStateProps } from '@/types/pathwayState'; +import { PolicyCreationPayload } from '@/types/payloads'; +import { Parameter } from '@/types/subIngredients/parameter'; +import { ValueInterval, ValueIntervalCollection } from '@/types/subIngredients/valueInterval'; +import { countPolicyModifications } from '@/utils/countParameterChanges'; +import { formatPeriod } from '@/utils/dateUtils'; +import { + formatLabelParts, + getHierarchicalLabels, + getHierarchicalLabelsFromTree, +} from '@/utils/parameterLabels'; +import { formatParameterValue } from '@/utils/policyTableHelpers'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../constants'; +import { createCurrentLawPolicy } from '../currentLaw'; +import { BrowseModalTemplate } from './BrowseModalTemplate'; +import { + PolicyBrowseContent, + PolicyCreationContent, + PolicyDetailsDrawer, + PolicyParameterTree, +} from './policy'; +import { PolicyOverviewContent } from './policyCreation'; +import type { EditorMode, ModifiedParam, SidebarTab } from './policyCreation/types'; + +interface PolicyBrowseModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (policy: PolicyStateProps) => void; +} + +export function PolicyBrowseModal({ isOpen, onClose, onSelect }: PolicyBrowseModalProps) { + const countryId = useCurrentCountry(); + const userId = MOCK_USER_ID.toString(); + const { data: policies, isLoading } = useUserPolicies(userId); + const { + parameterTree, + parameters, + loading: metadataLoading, + } = useSelector((state: RootState) => state.metadata); + const { minDate, maxDate } = useSelector(getDateRange); + const updatePolicyAssociation = useUpdatePolicyAssociation(); + + // Browse mode state + const [searchQuery, setSearchQuery] = useState(''); + const [activeSection, setActiveSection] = useState<'frequently-selected' | 'my-policies'>( + 'frequently-selected' + ); + const [selectedPolicyId, setSelectedPolicyId] = useState(null); + const [drawerPolicyId, setDrawerPolicyId] = useState(null); + + // Creation/editor mode state + const [isCreationMode, setIsCreationMode] = useState(false); + const [editorMode, setEditorMode] = useState('create'); + const [editingAssociationId, setEditingAssociationId] = useState(null); + const [isUpdating, setIsUpdating] = useState(false); + const [activeTab, setActiveTab] = useState('overview'); + const [policyLabel, setPolicyLabel] = useState(''); + const [policyParameters, setPolicyParameters] = useState([]); + const [selectedParam, setSelectedParam] = useState(null); + const [expandedMenuItems, setExpandedMenuItems] = useState>(new Set()); + const [valueSetterMode, setValueSetterMode] = useState(ValueSetterMode.DEFAULT); + const [intervals, setIntervals] = useState([]); + const [startDate, setStartDate] = useState('2025-01-01'); + const [endDate, setEndDate] = useState('2025-12-31'); + const [parameterSearch, setParameterSearch] = useState(''); + const [hoveredParamName, setHoveredParamName] = useState(null); + const [footerHovered, setFooterHovered] = useState(false); + const [originalLabel, setOriginalLabel] = useState(''); + const [showSameNameWarning, setShowSameNameWarning] = useState(false); + + // API hook for creating policy + const { createPolicy, isPending: isCreating } = useCreatePolicy(policyLabel || undefined); + + const isReadOnly = editorMode === 'display'; + + // editingAssociationId tracks the UserPolicy association being edited + // for "Update existing policy" functionality + + // Reset state on mount + useEffect(() => { + if (isOpen) { + setSearchQuery(''); + setActiveSection('frequently-selected'); + setSelectedPolicyId(null); + setDrawerPolicyId(null); + setIsCreationMode(false); + setEditorMode('create'); + + setEditingAssociationId(null); + setIsUpdating(false); + setActiveTab('overview'); + setPolicyLabel(''); + setPolicyParameters([]); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + } + }, [isOpen]); + + // Transform policies data, sorted by most recent + const userPolicies = useMemo(() => { + return (policies || []) + .map((p) => { + const policyId = p.association.policyId.toString(); + const label = p.association.label || `Policy #${policyId}`; + return { + id: policyId, + associationId: p.association.id, + label, + paramCount: countPolicyModifications(p.policy), + parameters: p.policy?.parameters || [], + createdAt: p.association.createdAt, + updatedAt: p.association.updatedAt, + }; + }) + .sort((a, b) => { + const aTime = a.updatedAt || a.createdAt || ''; + const bTime = b.updatedAt || b.createdAt || ''; + return bTime.localeCompare(aTime); + }); + }, [policies]); + + // Filter policies based on search + const filteredPolicies = useMemo(() => { + let result = userPolicies; + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + result = result.filter((p) => { + if (p.label.toLowerCase().includes(query)) { + return true; + } + const paramDisplayNames = p.parameters + .map((param) => { + const hierarchicalLabels = getHierarchicalLabelsFromTree(param.name, parameterTree); + return hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : param.name.split('.').pop() || param.name; + }) + .join(' ') + .toLowerCase(); + if (paramDisplayNames.includes(query)) { + return true; + } + return false; + }); + } + return result; + }, [userPolicies, searchQuery, parameterTree]); + + // Get policies for current section + const displayedPolicies = useMemo(() => { + return filteredPolicies; + }, [filteredPolicies]); + + // Get section title + const getSectionTitle = () => { + switch (activeSection) { + case 'my-policies': + return 'My policies'; + default: + return 'Policies'; + } + }; + + // Handle policy selection + const handleSelectPolicy = (policy: { + id: string; + label: string; + paramCount: number; + associationId?: string; + }) => { + if (policy.associationId) { + updatePolicyAssociation.mutate({ + userPolicyId: policy.associationId, + updates: {}, + }); + } + onSelect({ id: policy.id, label: policy.label, parameters: Array(policy.paramCount).fill({}) }); + onClose(); + }; + + // Handle current law selection + const handleSelectCurrentLaw = () => { + onSelect(createCurrentLawPolicy()); + onClose(); + }; + + // ========== Creation Mode Logic ========== + + // Create local policy state object + const localPolicy: PolicyStateProps = useMemo( + () => ({ + label: policyLabel, + parameters: policyParameters, + }), + [policyLabel, policyParameters] + ); + + // Count modifications + const modificationCount = countPolicyModifications(localPolicy); + + // Get modified parameter data for the overview tab grid + const modifiedParams: ModifiedParam[] = useMemo(() => { + return policyParameters.map((p) => { + const metadata = parameters[p.name]; + const hierarchicalLabels = getHierarchicalLabels(p.name, parameters); + const displayLabel = + hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : p.name.split('.').pop() || p.name; + const changes = p.values.map((interval) => ({ + period: formatPeriod(interval.startDate, interval.endDate), + value: formatParameterValue(interval.value, metadata?.unit), + })); + return { paramName: p.name, label: displayLabel, changes }; + }); + }, [policyParameters, parameters]); + + // Handle search selection + const handleSearchSelect = useCallback( + (paramName: string) => { + const param = parameters[paramName]; + if (!param || param.type !== 'parameter') { + return; + } + const pathParts = paramName.split('.'); + const newExpanded = new Set(expandedMenuItems); + let currentPath = ''; + for (let i = 0; i < pathParts.length - 1; i++) { + currentPath = currentPath ? `${currentPath}.${pathParts[i]}` : pathParts[i]; + newExpanded.add(currentPath); + } + setExpandedMenuItems(newExpanded); + setSelectedParam(param); + setIntervals([]); + setValueSetterMode(ValueSetterMode.DEFAULT); + setParameterSearch(''); + setActiveTab('parameters'); + }, + [parameters, expandedMenuItems] + ); + + // Handle menu item click + const handleMenuItemClick = useCallback( + (paramName: string) => { + const param = parameters[paramName]; + if (param && param.type === 'parameter') { + setSelectedParam(param); + setIntervals([]); + setValueSetterMode(ValueSetterMode.DEFAULT); + setActiveTab('parameters'); + } + setExpandedMenuItems((prev) => { + const newSet = new Set(prev); + if (newSet.has(paramName)) { + newSet.delete(paramName); + } else { + newSet.add(paramName); + } + return newSet; + }); + }, + [parameters] + ); + + // Handle value submission + const handleValueSubmit = useCallback(() => { + if (!selectedParam || intervals.length === 0) { + return; + } + const updatedParameters = [...policyParameters]; + let existingParam = updatedParameters.find((p) => p.name === selectedParam.parameter); + if (!existingParam) { + existingParam = { name: selectedParam.parameter, values: [] }; + updatedParameters.push(existingParam); + } + const paramCollection = new ValueIntervalCollection(existingParam.values); + intervals.forEach((interval) => { + paramCollection.addInterval(interval); + }); + existingParam.values = paramCollection.getIntervals(); + setPolicyParameters(updatedParameters); + setIntervals([]); + }, [selectedParam, intervals, policyParameters]); + + // Handle entering creation mode (new policy) + const handleEnterCreationMode = useCallback(() => { + setPolicyLabel(''); + setPolicyParameters([]); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + setActiveTab('overview'); + setEditorMode('create'); + setEditingAssociationId(null); + setIsUpdating(false); + setIsCreationMode(true); + }, []); + + // Handle opening an existing policy in the editor (display mode) + const handleOpenInEditor = useCallback( + (policy: { id: string; associationId?: string; label: string; parameters: Parameter[] }) => { + setDrawerPolicyId(null); + setPolicyLabel(policy.label); + setOriginalLabel(policy.label); + setPolicyParameters(policy.parameters); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + setActiveTab('overview'); + setEditorMode('display'); + setEditingAssociationId(policy.associationId || null); + setIsCreationMode(true); + }, + [] + ); + + // Exit editor / creation mode + const handleExitCreationMode = useCallback(() => { + setIsCreationMode(false); + setPolicyLabel(''); + setPolicyParameters([]); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + setEditorMode('create'); + + setEditingAssociationId(null); + setIsUpdating(false); + }, []); + + // Handle policy creation + const handleCreatePolicy = useCallback(async () => { + if (!policyLabel.trim()) { + return; + } + const policyData: Partial = { parameters: policyParameters }; + const payload: PolicyCreationPayload = PolicyAdapter.toCreationPayload(policyData as Policy); + try { + const result = await createPolicy(payload); + const createdPolicy: PolicyStateProps = { + id: result.result.policy_id, + label: policyLabel, + parameters: policyParameters, + }; + onSelect(createdPolicy); + onClose(); + } catch (error) { + console.error('Failed to create policy:', error); + } + }, [policyLabel, policyParameters, createPolicy, onSelect, onClose]); + + // Same-name guard for "Save as new policy" + const handleSaveAsNewPolicy = useCallback(() => { + const currentName = (policyLabel || '').trim(); + const origName = (originalLabel || '').trim(); + if (editorMode === 'edit' && currentName && currentName === origName) { + setShowSameNameWarning(true); + } else { + handleCreatePolicy(); + } + }, [policyLabel, originalLabel, editorMode, handleCreatePolicy]); + + // Handle updating an existing policy (create new base policy, update association) + const handleUpdateExistingPolicy = useCallback(async () => { + if (!policyLabel.trim() || !editingAssociationId) { + return; + } + setIsUpdating(true); + + const policyData: Partial = { parameters: policyParameters }; + const payload: PolicyCreationPayload = PolicyAdapter.toCreationPayload(policyData as Policy); + + try { + const result = await createPolicyApi(countryId, payload); + const newPolicyId = result.result.policy_id; + + await updatePolicyAssociation.mutateAsync({ + userPolicyId: editingAssociationId, + updates: { policyId: newPolicyId, label: policyLabel }, + }); + + onSelect({ + id: newPolicyId, + label: policyLabel, + parameters: policyParameters, + }); + onClose(); + } catch (error) { + console.error('Failed to update policy:', error); + setIsUpdating(false); + } + }, [ + policyLabel, + policyParameters, + editingAssociationId, + countryId, + updatePolicyAssociation, + onSelect, + onClose, + ]); + + // Policy for drawer preview + const drawerPolicy = useMemo(() => { + if (!drawerPolicyId) { + return null; + } + return userPolicies.find((p) => p.id === drawerPolicyId) || null; + }, [drawerPolicyId, userPolicies]); + + const colorConfig = INGREDIENT_COLORS.policy; + + // ========== Sidebar Rendering ========== + + // Browse mode sidebar sections + const browseSidebarSections = useMemo( + () => [ + { + id: 'library', + label: 'Library', + items: [ + { + id: 'frequently-selected', + label: 'Frequently selected', + icon: , + isActive: activeSection === 'frequently-selected', + onClick: () => setActiveSection('frequently-selected'), + }, + { + id: 'my-policies', + label: 'My policies', + icon: , + badge: userPolicies.length, + isActive: activeSection === 'my-policies', + onClick: () => setActiveSection('my-policies'), + }, + { + id: 'create-new', + label: 'Create new policy', + icon: , + isActive: isCreationMode, + onClick: handleEnterCreationMode, + }, + ], + }, + ], + [activeSection, userPolicies.length, isCreationMode, handleEnterCreationMode] + ); + + // Creation mode custom sidebar + const renderCreationSidebar = () => ( + + ); + + // ========== Main Content Rendering ========== + + // Overview content for creation mode — naming, param grid, action buttons + const renderOverviewContent = () => ( + + + + + + ); + + const renderMainContent = () => { + if (isCreationMode) { + if (activeTab === 'overview') { + return renderOverviewContent(); + } + return ( + + ); + } + + if (activeSection === 'frequently-selected') { + return ( + + + Frequently selected + + + + + + + Current law + + + No parameter changes + + + + + + + ); + } + + return ( + <> + { + setSelectedPolicyId(policy.id); + handleSelectPolicy(policy); + }} + onPolicyInfoClick={(policyId) => setDrawerPolicyId(policyId)} + onEnterCreationMode={handleEnterCreationMode} + getSectionTitle={getSectionTitle} + /> + setDrawerPolicyId(null)} + onSelect={() => { + if (drawerPolicy) { + handleSelectPolicy(drawerPolicy); + setDrawerPolicyId(null); + } + }} + onEdit={() => { + if (drawerPolicy) { + handleOpenInEditor(drawerPolicy); + } + }} + /> + + ); + }; + + // ========== Render ========== + + // Header title based on editor mode + const getEditorHeaderTitle = () => { + if (editorMode === 'display') { + return 'Policy details'; + } + if (editorMode === 'edit') { + return 'Edit policy'; + } + return 'Policy editor'; + }; + + return ( + <> + } + headerTitle={isCreationMode ? getEditorHeaderTitle() : 'Select policy'} + headerSubtitle={ + isCreationMode ? undefined : 'Choose an existing policy or create a new one' + } + colorConfig={colorConfig} + sidebarSections={isCreationMode ? undefined : browseSidebarSections} + renderSidebar={isCreationMode ? renderCreationSidebar : undefined} + sidebarWidth={isCreationMode ? 280 : undefined} + renderMainContent={renderMainContent} + footer={ + isCreationMode ? ( + + + + {modificationCount > 0 && ( + setActiveTab('overview')} + onMouseEnter={() => setFooterHovered(true)} + onMouseLeave={() => setFooterHovered(false)} + > + + + {modificationCount} parameter{modificationCount !== 1 ? 's' : ''} modified + + + )} + + + {editorMode === 'create' && ( + + )} + {editorMode === 'display' && ( + setEditorMode('edit')} + /> + )} + {editorMode === 'edit' && ( + <> + + + + )} + + + ) : undefined + } + contentPadding={isCreationMode ? 0 : undefined} + /> + + setShowSameNameWarning(false)} + title="Same name" + centered + size="sm" + > + + + Both the original and new policy will have the name "{policyLabel.trim()}". + Are you sure you want to save? + + + + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/PolicyCreationModal.tsx b/app/src/pages/reportBuilder/modals/PolicyCreationModal.tsx new file mode 100644 index 000000000..5652c771c --- /dev/null +++ b/app/src/pages/reportBuilder/modals/PolicyCreationModal.tsx @@ -0,0 +1,687 @@ +/** + * PolicyCreationModal - Standalone policy creation modal + * + * This modal provides: + * - Parameter tree navigation (sidebar) with "Policy overview" menu item + * - Overview shows naming, param grid, and action buttons in main area + * - Selecting a parameter shows the value setter / chart editor in main area + * - Policy creation with API integration + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { IconScale, IconX } from '@tabler/icons-react'; +import { useSelector } from 'react-redux'; +import { ActionIcon, Box, Button, Group, Modal, Stack, Text } from '@mantine/core'; +import { PolicyAdapter } from '@/adapters'; +import { createPolicy as createPolicyApi } from '@/api/policy'; +import { + EditAndSaveNewButton, + EditAndUpdateButton, + EditDefaultButton, +} from '@/components/common/ActionButtons'; +import { colors, spacing } from '@/designTokens'; +import { useCreatePolicy } from '@/hooks/useCreatePolicy'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useUpdatePolicyAssociation } from '@/hooks/useUserPolicy'; +import { getDateRange, selectSearchableParameters } from '@/libs/metadataUtils'; +import { ValueSetterMode } from '@/pathways/report/components/valueSetters'; +import { RootState } from '@/store'; +import { Policy } from '@/types/ingredients/Policy'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { PolicyStateProps } from '@/types/pathwayState'; +import { PolicyCreationPayload } from '@/types/payloads'; +import { Parameter } from '@/types/subIngredients/parameter'; +import { + ValueInterval, + ValueIntervalCollection, + ValuesList, +} from '@/types/subIngredients/valueInterval'; +import { countPolicyModifications } from '@/utils/countParameterChanges'; +import { formatPeriod } from '@/utils/dateUtils'; +import { formatLabelParts, getHierarchicalLabels } from '@/utils/parameterLabels'; +import { formatParameterValue } from '@/utils/policyTableHelpers'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../constants'; +import { + ChangesCard, + EditorMode, + EmptyParameterState, + HistoricalValuesCard, + ModifiedParam, + ParameterHeaderCard, + ParameterSidebar, + PolicyOverviewContent, + SidebarTab, + ValueSetterCard, +} from './policyCreation'; + +interface PolicyCreationModalProps { + isOpen: boolean; + onClose: () => void; + onPolicyCreated: (policy: PolicyStateProps) => void; + simulationIndex: number; + initialPolicy?: PolicyStateProps; + initialEditorMode?: EditorMode; + initialAssociationId?: string; +} + +export function PolicyCreationModal({ + isOpen, + onClose, + onPolicyCreated, + simulationIndex, + initialPolicy, + initialEditorMode, + initialAssociationId, +}: PolicyCreationModalProps) { + const countryId = useCurrentCountry(); + + // Get metadata from Redux state + const { + parameterTree, + parameters, + loading: metadataLoading, + } = useSelector((state: RootState) => state.metadata); + const { minDate, maxDate } = useSelector(getDateRange); + + // Local policy state + const [policyLabel, setPolicyLabel] = useState(''); + const [policyParameters, setPolicyParameters] = useState([]); + + // Sidebar tab state — controls main content area + const [activeTab, setActiveTab] = useState('overview'); + + // Parameter selection state + const [selectedParam, setSelectedParam] = useState(null); + const [expandedMenuItems, setExpandedMenuItems] = useState>(new Set()); + + // Value setter state + const [valueSetterMode, setValueSetterMode] = useState(ValueSetterMode.DEFAULT); + const [intervals, setIntervals] = useState([]); + const [startDate, setStartDate] = useState('2025-01-01'); + const [endDate, setEndDate] = useState('2025-12-31'); + + // Parameter search state + const [parameterSearch, setParameterSearch] = useState(''); + const [hoveredParamName, setHoveredParamName] = useState(null); + const [footerHovered, setFooterHovered] = useState(false); + + // API hooks + const { createPolicy, isPending: isCreating } = useCreatePolicy(policyLabel || undefined); + const updatePolicyAssociation = useUpdatePolicyAssociation(); + const [isUpdating, setIsUpdating] = useState(false); + + // Suppress unused variable warning + void simulationIndex; + + // Editor mode: create (new policy), display (read-only existing), edit (modifying existing) + const [editorMode, setEditorMode] = useState( + initialEditorMode || (initialPolicy ? 'edit' : 'create') + ); + const isReadOnly = editorMode === 'display'; + const colorConfig = INGREDIENT_COLORS.policy; + + // Reset state when modal opens; pre-populate from initialPolicy when editing + useEffect(() => { + if (isOpen) { + setPolicyLabel(initialPolicy?.label || ''); + setPolicyParameters(initialPolicy?.parameters || []); + setEditorMode(initialEditorMode || (initialPolicy ? 'edit' : 'create')); + setActiveTab('overview'); + setSelectedParam(null); + setExpandedMenuItems(new Set()); + setIntervals([]); + setParameterSearch(''); + } + }, [isOpen, initialEditorMode]); // initialPolicy intentionally not in deps — only read on open transition + + // Create local policy state object for components + const localPolicy: PolicyStateProps = useMemo( + () => ({ + label: policyLabel, + parameters: policyParameters, + }), + [policyLabel, policyParameters] + ); + + // Count modifications + const modificationCount = countPolicyModifications(localPolicy); + + // Get modified parameter data for the Changes section + const modifiedParams: ModifiedParam[] = useMemo(() => { + return policyParameters.map((p) => { + const metadata = parameters[p.name]; + const hierarchicalLabels = getHierarchicalLabels(p.name, parameters); + const displayLabel = + hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : p.name.split('.').pop() || p.name; + + const changes = p.values.map((interval) => ({ + period: formatPeriod(interval.startDate, interval.endDate), + value: formatParameterValue(interval.value, metadata?.unit), + })); + + return { + paramName: p.name, + label: displayLabel, + changes, + }; + }); + }, [policyParameters, parameters]); + + // Get searchable parameters from memoized selector + const searchableParameters = useSelector(selectSearchableParameters); + + // Handle search selection - expand tree path and select parameter + const handleSearchSelect = useCallback( + (paramName: string) => { + const param = parameters[paramName]; + if (!param || param.type !== 'parameter') { + return; + } + + const pathParts = paramName.split('.'); + const newExpanded = new Set(expandedMenuItems); + let currentPath = ''; + for (let i = 0; i < pathParts.length - 1; i++) { + currentPath = currentPath ? `${currentPath}.${pathParts[i]}` : pathParts[i]; + newExpanded.add(currentPath); + } + setExpandedMenuItems(newExpanded); + setSelectedParam(param); + setIntervals([]); + setValueSetterMode(ValueSetterMode.DEFAULT); + setParameterSearch(''); + // Auto-switch to parameters tab when selecting from search + setActiveTab('parameters'); + }, + [parameters, expandedMenuItems] + ); + + // Handle menu item click + const handleMenuItemClick = useCallback( + (paramName: string) => { + const param = parameters[paramName]; + if (param && param.type === 'parameter') { + setSelectedParam(param); + setIntervals([]); + setValueSetterMode(ValueSetterMode.DEFAULT); + // Auto-switch to parameters tab when selecting a parameter + setActiveTab('parameters'); + } + setExpandedMenuItems((prev) => { + const newSet = new Set(prev); + if (newSet.has(paramName)) { + newSet.delete(paramName); + } else { + newSet.add(paramName); + } + return newSet; + }); + }, + [parameters] + ); + + // Handle parameter selection from changes card + const handleSelectParam = useCallback( + (paramName: string) => { + const metadata = parameters[paramName]; + if (metadata) { + setSelectedParam(metadata); + } + }, + [parameters] + ); + + // Handle value submission + const handleValueSubmit = useCallback(() => { + if (!selectedParam || intervals.length === 0) { + return; + } + + const updatedParameters = [...policyParameters]; + let existingParam = updatedParameters.find((p) => p.name === selectedParam.parameter); + + if (!existingParam) { + existingParam = { name: selectedParam.parameter, values: [] }; + updatedParameters.push(existingParam); + } + + const paramCollection = new ValueIntervalCollection(existingParam.values); + intervals.forEach((interval) => { + paramCollection.addInterval(interval); + }); + + existingParam.values = paramCollection.getIntervals(); + setPolicyParameters(updatedParameters); + setIntervals([]); + }, [selectedParam, intervals, policyParameters]); + + // Handle policy creation + const handleCreatePolicy = useCallback(async () => { + const policyData: Partial = { + parameters: policyParameters, + }; + + const payload: PolicyCreationPayload = PolicyAdapter.toCreationPayload(policyData as Policy); + + try { + const result = await createPolicy(payload); + const createdPolicy: PolicyStateProps = { + id: result.result.policy_id, + label: policyLabel || null, + parameters: policyParameters, + }; + onPolicyCreated(createdPolicy); + onClose(); + } catch (error) { + console.error('Failed to create policy:', error); + } + }, [policyLabel, policyParameters, createPolicy, onPolicyCreated, onClose]); + + // Same-name warning for "Save as new" when name matches original + const [showSameNameWarning, setShowSameNameWarning] = useState(false); + + // Unnamed-policy warning for creating/saving without a name + const [showUnnamedWarning, setShowUnnamedWarning] = useState(false); + + const handleSaveAsNewPolicy = useCallback(() => { + const currentName = (policyLabel || '').trim(); + const originalName = (initialPolicy?.label || '').trim(); + if (editorMode === 'edit' && currentName && currentName === originalName) { + setShowSameNameWarning(true); + } else { + handleCreatePolicy(); + } + }, [policyLabel, initialPolicy?.label, editorMode, handleCreatePolicy]); + + // Handle updating an existing policy (create new base policy, update association) + const handleUpdateExistingPolicy = useCallback(async () => { + if (!policyLabel.trim() || !initialAssociationId) { + return; + } + setIsUpdating(true); + + const policyData: Partial = { parameters: policyParameters }; + const payload: PolicyCreationPayload = PolicyAdapter.toCreationPayload(policyData as Policy); + + try { + const result = await createPolicyApi(countryId, payload); + const newPolicyId = result.result.policy_id; + + await updatePolicyAssociation.mutateAsync({ + userPolicyId: initialAssociationId, + updates: { policyId: newPolicyId, label: policyLabel }, + }); + + onPolicyCreated({ + id: newPolicyId, + label: policyLabel || null, + parameters: policyParameters, + }); + onClose(); + } catch (error) { + console.error('Failed to update policy:', error); + setIsUpdating(false); + } + }, [ + policyLabel, + policyParameters, + initialAssociationId, + countryId, + updatePolicyAssociation, + onPolicyCreated, + onClose, + ]); + + // Get base and reform values for chart + const getChartValues = () => { + if (!selectedParam) { + return { baseValues: null, reformValues: null }; + } + + const baseValues = new ValueIntervalCollection(selectedParam.values as ValuesList); + const reformValues = new ValueIntervalCollection(baseValues); + + const paramToChart = policyParameters.find((p) => p.name === selectedParam.parameter); + if (paramToChart && paramToChart.values && paramToChart.values.length > 0) { + const userIntervals = new ValueIntervalCollection(paramToChart.values as ValuesList); + for (const interval of userIntervals.getIntervals()) { + reformValues.addInterval(interval); + } + } + + return { baseValues, reformValues }; + }; + + const { baseValues, reformValues } = getChartValues(); + + // ========================================================================= + // OVERVIEW CONTENT — shown when "Policy overview" tab is active + // ========================================================================= + + const renderOverviewContent = () => ( + + + + + + ); + + // ========================================================================= + // PARAMETERS CONTENT — shown when "Parameters" tab is active + // ========================================================================= + + const renderParametersContent = () => ( + + {!selectedParam ? ( + + ) : ( + + + + + + {!isReadOnly && ( + + )} + p.paramName === selectedParam?.parameter + )} + modificationCount={modificationCount} + selectedParamName={selectedParam?.parameter} + onSelectParam={handleSelectParam} + /> + + + + + + )} + + ); + + return ( + + + + + + + {editorMode === 'display' + ? 'Policy details' + : editorMode === 'edit' + ? 'Edit policy' + : 'Policy editor'} + + + + + + + } + > + {/* Main content area */} + + {/* Left Sidebar - Parameter Tree with tabs */} + + + {/* Main Content — switches based on active tab */} + {activeTab === 'overview' ? renderOverviewContent() : renderParametersContent()} + + + {/* Footer — unified mode bar */} + + + + + {modificationCount > 0 && ( + setActiveTab('overview')} + onMouseEnter={() => setFooterHovered(true)} + onMouseLeave={() => setFooterHovered(false)} + > + + + {modificationCount} parameter{modificationCount !== 1 ? 's' : ''} modified + + + )} + + + {editorMode === 'create' && ( + + )} + {editorMode === 'display' && ( + setEditorMode('edit')} + /> + )} + {editorMode === 'edit' && ( + <> + + { + if (!policyLabel.trim()) { + setShowUnnamedWarning(true); + } else { + handleSaveAsNewPolicy(); + } + }} + loading={isCreating} + disabled={isUpdating} + /> + + )} + + + + + {/* Same-name warning modal */} + setShowSameNameWarning(false)} + title={Same name} + centered + size="sm" + > + + + Both the original and new policy will have the name “ + {policyLabel}”. Are you sure you want to save? + + + + + + + + + {/* Unnamed policy warning modal */} + setShowUnnamedWarning(false)} + title={Unnamed policy} + centered + size="sm" + > + + + This policy has no name. Are you sure you want to save it without a name? + + + + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/PopulationBrowseModal.tsx b/app/src/pages/reportBuilder/modals/PopulationBrowseModal.tsx new file mode 100644 index 000000000..a05741494 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/PopulationBrowseModal.tsx @@ -0,0 +1,623 @@ +/** + * PopulationBrowseModal - Geography and household selection modal + * + * Uses BrowseModalTemplate for visual layout and delegates to sub-components: + * - Browse mode: PopulationBrowseContent for main content + * - Creation mode: HouseholdCreationContent + PopulationStatusHeader + */ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + IconChevronRight, + IconFolder, + IconHome, + IconPlus, + IconStar, + IconUsers, +} from '@tabler/icons-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; +import { Box, Group, Paper, Stack, Text } from '@mantine/core'; +import { HouseholdAdapter } from '@/adapters/HouseholdAdapter'; +import { geographyUsageStore, householdUsageStore } from '@/api/usageTracking'; +import { UKOutlineIcon, USOutlineIcon } from '@/components/icons/CountryOutlineIcons'; +import { CURRENT_YEAR, MOCK_USER_ID } from '@/constants'; +import { colors, spacing } from '@/designTokens'; +import { useCreateHousehold } from '@/hooks/useCreateHousehold'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import { useUserHouseholds } from '@/hooks/useUserHousehold'; +import { getBasicInputFields } from '@/libs/metadataUtils'; +import { householdAssociationKeys } from '@/libs/queryKeys'; +import { RootState } from '@/store'; +import { Geography } from '@/types/ingredients/Geography'; +import { Household } from '@/types/ingredients/Household'; +import { PopulationStateProps } from '@/types/pathwayState'; +import { generateGeographyLabel } from '@/utils/geographyUtils'; +import { HouseholdBuilder } from '@/utils/HouseholdBuilder'; +import { + getUKConstituencies, + getUKCountries, + getUKLocalAuthorities, + getUSCongressionalDistricts, + getUSPlaces, + getUSStates, + RegionOption, +} from '@/utils/regionStrategies'; +import { COUNTRY_CONFIG, FONT_SIZES, INGREDIENT_COLORS } from '../constants'; +import { PopulationCategory } from '../types'; +import { BrowseModalTemplate, CreationModeFooter } from './BrowseModalTemplate'; +import { + HouseholdCreationContent, + PopulationBrowseContent, + PopulationStatusHeader, +} from './population'; + +interface PopulationBrowseModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (population: PopulationStateProps) => void; + onCreateNew: () => void; +} + +export function PopulationBrowseModal({ + isOpen, + onClose, + onSelect, + onCreateNew: _onCreateNew, +}: PopulationBrowseModalProps) { + const countryId = useCurrentCountry() as 'us' | 'uk'; + const userId = MOCK_USER_ID.toString(); + const queryClient = useQueryClient(); + const { data: households, isLoading: householdsLoading } = useUserHouseholds(userId); + const regionOptions = useSelector((state: RootState) => state.metadata.economyOptions.region); + const metadata = useSelector((state: RootState) => state.metadata); + const basicInputFields = useSelector(getBasicInputFields); + + // State + const [searchQuery, setSearchQuery] = useState(''); + const [activeCategory, setActiveCategory] = useState('frequently-selected'); + + // Creation mode state + const [isCreationMode, setIsCreationMode] = useState(false); + const [householdLabel, setHouseholdLabel] = useState(''); + const [householdDraft, setHouseholdDraft] = useState(null); + + // Get report year (default to current year) + const reportYear = CURRENT_YEAR.toString(); + + // Create household hook + const { createHousehold, isPending: isCreating } = useCreateHousehold( + householdLabel || undefined + ); + + // Get all basic non-person fields dynamically + const basicNonPersonFields = useMemo(() => { + return Object.entries(basicInputFields) + .filter(([key]) => key !== 'person') + .flatMap(([, fields]) => fields); + }, [basicInputFields]); + + // Derive marital status and number of children from household draft + const householdPeople = useMemo(() => { + if (!householdDraft) { + return []; + } + return Object.keys(householdDraft.householdData.people || {}); + }, [householdDraft]); + + const maritalStatus = householdPeople.includes('your partner') ? 'married' : 'single'; + const numChildren = householdPeople.filter((p) => p.includes('dependent')).length; + + // Reset state on mount + useEffect(() => { + if (isOpen) { + setSearchQuery(''); + setActiveCategory('frequently-selected'); + setIsCreationMode(false); + setHouseholdLabel(''); + setHouseholdDraft(null); + } + }, [isOpen, countryId]); + + // Get geography categories based on country + const geographyCategories = useMemo(() => { + if (countryId === 'uk') { + const ukCountries = getUKCountries(regionOptions); + const ukConstituencies = getUKConstituencies(regionOptions); + const ukLocalAuthorities = getUKLocalAuthorities(regionOptions); + return [ + { + id: 'countries' as const, + label: 'Countries', + count: ukCountries.length, + regions: ukCountries, + }, + { + id: 'constituencies' as const, + label: 'Constituencies', + count: ukConstituencies.length, + regions: ukConstituencies, + }, + { + id: 'local-authorities' as const, + label: 'Local authorities', + count: ukLocalAuthorities.length, + regions: ukLocalAuthorities, + }, + ]; + } + // US + const usStates = getUSStates(regionOptions); + const usDistricts = getUSCongressionalDistricts(regionOptions); + const usPlaces = getUSPlaces(); + return [ + { + id: 'states' as const, + label: 'States and territories', + count: usStates.length, + regions: usStates, + }, + { + id: 'districts' as const, + label: 'Congressional districts', + count: usDistricts.length, + regions: usDistricts, + }, + { + id: 'places' as const, + label: 'Cities', + count: usPlaces.length, + regions: usPlaces, + }, + ]; + }, [countryId, regionOptions]); + + // Get regions for active category + const activeRegions = useMemo(() => { + const category = geographyCategories.find((c) => c.id === activeCategory); + return category?.regions || []; + }, [activeCategory, geographyCategories]); + + // Transform households with usage tracking sort + const sortedHouseholds = useMemo(() => { + if (!households) { + return []; + } + + return [...households] + .map((h) => { + const householdIdStr = String(h.association.householdId); + const usageTimestamp = householdUsageStore.getLastUsed(householdIdStr); + const sortTimestamp = + usageTimestamp || h.association.updatedAt || h.association.createdAt || ''; + return { + id: householdIdStr, + label: h.association.label || `Household #${householdIdStr}`, + memberCount: h.household?.household_json?.people + ? Object.keys(h.household.household_json.people).length + : 0, + sortTimestamp, + household: h.household, + }; + }) + .sort((a, b) => b.sortTimestamp.localeCompare(a.sortTimestamp)); + }, [households]); + + // Filter regions/households based on search + const filteredRegions = useMemo(() => { + if (!searchQuery.trim()) { + return activeRegions; + } + const query = searchQuery.toLowerCase(); + return activeRegions.filter((r) => r.label.toLowerCase().includes(query)); + }, [activeRegions, searchQuery]); + + const filteredHouseholds = useMemo(() => { + if (!searchQuery.trim()) { + return sortedHouseholds; + } + const query = searchQuery.toLowerCase(); + return sortedHouseholds.filter((h) => h.label.toLowerCase().includes(query)); + }, [sortedHouseholds, searchQuery]); + + // Handle geography selection + const handleSelectGeography = (region: RegionOption | null) => { + const countryConfig = COUNTRY_CONFIG[countryId]; + const geography: Geography = region + ? { + id: `${countryId}-${region.value}`, + countryId, + scope: 'subnational', + geographyId: region.value, + name: region.label, + } + : { + id: countryConfig.geographyId, + countryId, + scope: 'national', + geographyId: countryConfig.geographyId, + }; + + geographyUsageStore.recordUsage(geography.geographyId); + + const label = generateGeographyLabel(geography); + onSelect({ + geography, + household: null, + label, + type: 'geography', + }); + onClose(); + }; + + // Handle household selection + const handleSelectHousehold = (householdData: (typeof sortedHouseholds)[0]) => { + const householdIdStr = String(householdData.id); + householdUsageStore.recordUsage(householdIdStr); + + let household: Household | null = null; + if (householdData.household) { + household = HouseholdAdapter.fromMetadata(householdData.household); + } else { + household = { + id: householdIdStr, + countryId, + householdData: { people: {} }, + }; + } + + const populationState: PopulationStateProps = { + geography: null, + household, + label: householdData.label, + type: 'household', + }; + + onSelect(populationState); + onClose(); + }; + + // Enter creation mode + const handleEnterCreationMode = useCallback(() => { + const builder = new HouseholdBuilder(countryId as 'us' | 'uk', reportYear); + builder.addAdult('you', 30, { employment_income: 0 }); + setHouseholdDraft(builder.build()); + setHouseholdLabel(''); + setIsCreationMode(true); + }, [countryId, reportYear]); + + // Exit creation mode (back to browse) + const handleExitCreationMode = useCallback(() => { + setIsCreationMode(false); + setHouseholdDraft(null); + setHouseholdLabel(''); + }, []); + + // Handle marital status change + const handleMaritalStatusChange = useCallback( + (newStatus: 'single' | 'married') => { + if (!householdDraft) { + return; + } + + const builder = new HouseholdBuilder(countryId as 'us' | 'uk', reportYear); + builder.loadHousehold(householdDraft); + + const hasPartner = householdPeople.includes('your partner'); + + if (newStatus === 'married' && !hasPartner) { + builder.addAdult('your partner', 30, { employment_income: 0 }); + builder.setMaritalStatus('you', 'your partner'); + } else if (newStatus === 'single' && hasPartner) { + builder.removePerson('your partner'); + } + + setHouseholdDraft(builder.build()); + }, + [householdDraft, householdPeople, countryId, reportYear] + ); + + // Handle number of children change + const handleNumChildrenChange = useCallback( + (newCount: number) => { + if (!householdDraft) { + return; + } + + const builder = new HouseholdBuilder(countryId as 'us' | 'uk', reportYear); + builder.loadHousehold(householdDraft); + + const currentChildren = householdPeople.filter((p) => p.includes('dependent')); + const currentChildCount = currentChildren.length; + + if (newCount !== currentChildCount) { + currentChildren.forEach((child) => builder.removePerson(child)); + + if (newCount > 0) { + const hasPartner = householdPeople.includes('your partner'); + const parentIds = hasPartner ? ['you', 'your partner'] : ['you']; + const ordinals = ['first', 'second', 'third', 'fourth', 'fifth']; + + for (let i = 0; i < newCount; i++) { + const childName = `your ${ordinals[i] || `${i + 1}th`} dependent`; + builder.addChild(childName, 10, parentIds, { employment_income: 0 }); + } + } + } + + setHouseholdDraft(builder.build()); + }, + [householdDraft, householdPeople, countryId, reportYear] + ); + + // Handle household creation submission + const handleCreateHousehold = useCallback(async () => { + if (!householdDraft || !householdLabel.trim()) { + return; + } + + const payload = HouseholdAdapter.toCreationPayload(householdDraft.householdData, countryId); + + try { + const result = await createHousehold(payload); + const householdId = result.result.household_id.toString(); + + householdUsageStore.recordUsage(householdId); + + const createdHousehold: Household = { + ...householdDraft, + id: householdId, + }; + + const populationState = { + geography: null, + household: createdHousehold, + label: householdLabel, + type: 'household' as const, + }; + + await queryClient.refetchQueries({ + queryKey: householdAssociationKeys.byUser(userId, countryId), + }); + + onSelect(populationState); + onClose(); + } catch (err) { + console.error('Failed to create household:', err); + } + }, [ + householdDraft, + householdLabel, + countryId, + createHousehold, + onSelect, + onClose, + queryClient, + userId, + ]); + + const colorConfig = INGREDIENT_COLORS.population; + + // Get section title + const getSectionTitle = () => { + if (activeCategory === 'my-households') { + return 'My households'; + } + const category = geographyCategories.find((c) => c.id === activeCategory); + return category?.label || 'Regions'; + }; + + // Get item count for display + const getItemCount = () => { + if (activeCategory === 'my-households') { + return filteredHouseholds.length; + } + return filteredRegions.length; + }; + + // ========== Sidebar Sections ========== + + const countryConfig = COUNTRY_CONFIG[countryId]; + + const browseSidebarSections = useMemo( + () => [ + { + id: 'geographies', + label: 'Geographies', + items: [ + { + id: 'frequently-selected', + label: 'Frequently selected', + icon: , + isActive: activeCategory === 'frequently-selected' && !isCreationMode, + onClick: () => { + setActiveCategory('frequently-selected'); + setIsCreationMode(false); + }, + }, + ...geographyCategories.map((category) => ({ + id: category.id, + label: category.label, + icon: , + badge: category.count, + isActive: activeCategory === category.id && !isCreationMode, + onClick: () => { + setActiveCategory(category.id); + setIsCreationMode(false); + }, + })), + ], + }, + { + id: 'households', + label: 'Households', + items: [ + { + id: 'my-households', + label: 'My households', + icon: , + badge: sortedHouseholds.length, + isActive: activeCategory === 'my-households' && !isCreationMode, + onClick: () => { + setActiveCategory('my-households'); + setIsCreationMode(false); + }, + }, + { + id: 'create-new', + label: 'Create new household', + icon: , + isActive: isCreationMode, + onClick: handleEnterCreationMode, + }, + ], + }, + ], + [ + activeCategory, + isCreationMode, + geographyCategories, + sortedHouseholds.length, + handleEnterCreationMode, + ] + ); + + // ========== Main Content Rendering ========== + + const renderMainContent = () => { + if (isCreationMode) { + return ( + + ); + } + + if (activeCategory === 'frequently-selected') { + return ( + + + Frequently selected + + handleSelectGeography(null)} + > + + + + {countryId === 'uk' ? : } + + + + {countryConfig.nationwideTitle} + + + {countryConfig.nationwideSubtitle} + + + + + + + + ); + } + + // Get all congressional districts for StateDistrictSelector (US only) + const allDistricts = + countryId === 'us' + ? geographyCategories.find((c) => c.id === 'districts')?.regions + : undefined; + + return ( + ({ + id: h.id, + label: h.label, + memberCount: h.memberCount, + }))} + householdsLoading={householdsLoading} + getSectionTitle={getSectionTitle} + getItemCount={getItemCount} + onSelectGeography={handleSelectGeography} + onSelectHousehold={(household) => { + const fullHousehold = sortedHouseholds.find((h) => h.id === household.id); + if (fullHousehold) { + handleSelectHousehold(fullHousehold); + } + }} + /> + ); + }; + + return ( + } + headerTitle={isCreationMode ? 'Create household' : 'Household(s)'} + headerSubtitle={ + isCreationMode + ? 'Configure your household composition and details' + : 'Choose a geographic region or create a household' + } + colorConfig={colorConfig} + sidebarSections={browseSidebarSections} + renderMainContent={renderMainContent} + statusHeader={ + isCreationMode ? ( + + ) : undefined + } + footer={ + isCreationMode ? ( + + ) : undefined + } + /> + ); +} diff --git a/app/src/pages/reportBuilder/modals/index.ts b/app/src/pages/reportBuilder/modals/index.ts new file mode 100644 index 000000000..225fe9297 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/index.ts @@ -0,0 +1,5 @@ +export { BrowseModalTemplate, CreationModeFooter } from './BrowseModalTemplate'; +export { IngredientPickerModal } from './IngredientPickerModal'; +export { PolicyBrowseModal } from './PolicyBrowseModal'; +export { PopulationBrowseModal } from './PopulationBrowseModal'; +export { PolicyCreationModal } from './PolicyCreationModal'; diff --git a/app/src/pages/reportBuilder/modals/policy/PolicyBrowseContent.tsx b/app/src/pages/reportBuilder/modals/policy/PolicyBrowseContent.tsx new file mode 100644 index 000000000..78e2e7eb7 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policy/PolicyBrowseContent.tsx @@ -0,0 +1,258 @@ +/** + * PolicyBrowseContent - Browse mode content (search bar + policy grid) + */ +import { + IconChevronRight, + IconFolder, + IconInfoCircle, + IconPlus, + IconSearch, + IconUsers, +} from '@tabler/icons-react'; +import { + ActionIcon, + Box, + Button, + Group, + Paper, + ScrollArea, + Skeleton, + Stack, + Text, + TextInput, +} from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +interface PolicyItem { + id: string; + associationId?: string; + label: string; + paramCount: number; + createdAt?: string; + updatedAt?: string; +} + +type ActiveSection = 'my-policies' | 'public'; + +interface PolicyBrowseContentProps { + displayedPolicies: PolicyItem[]; + searchQuery: string; + setSearchQuery: (query: string) => void; + activeSection: ActiveSection; + isLoading: boolean; + selectedPolicyId: string | null; + onSelectPolicy: (policy: PolicyItem) => void; + onPolicyInfoClick: (policyId: string) => void; + onEnterCreationMode: () => void; + getSectionTitle: () => string; +} + +export function PolicyBrowseContent({ + displayedPolicies, + searchQuery, + setSearchQuery, + activeSection, + isLoading, + selectedPolicyId, + onSelectPolicy, + onPolicyInfoClick, + onEnterCreationMode, + getSectionTitle, +}: PolicyBrowseContentProps) { + const colorConfig = INGREDIENT_COLORS.policy; + + const modalStyles = { + searchBar: { + position: 'relative' as const, + }, + policyGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', + gap: spacing.md, + }, + policyCard: { + background: colors.white, + border: `1px solid ${colors.border.light}`, + borderRadius: spacing.radius.lg, + padding: spacing.lg, + cursor: 'pointer', + transition: 'all 0.2s ease', + position: 'relative' as const, + overflow: 'hidden', + }, + }; + + return ( + + + } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + size="sm" + styles={{ + input: { + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + fontSize: FONT_SIZES.small, + }, + }} + /> + + + + + {getSectionTitle()} + + + {displayedPolicies.length} {displayedPolicies.length === 1 ? 'policy' : 'policies'} + + + + + {isLoading ? ( + + {[1, 2, 3].map((i) => ( + + ))} + + ) : activeSection === 'public' ? ( + + + + + + Coming soon + + + Search and browse policies created by other PolicyEngine users. + + + ) : displayedPolicies.length === 0 ? ( + + + + + + {searchQuery ? 'No policies match your search' : 'No policies yet'} + + + {searchQuery + ? 'Try adjusting your search terms or browse all policies' + : 'Create your first policy to get started'} + + {!searchQuery && ( + + )} + + ) : ( + + {displayedPolicies.map((policy) => { + const isSelected = selectedPolicyId === policy.id; + return ( + onSelectPolicy(policy)} + > + + + + + {policy.label} + + + {policy.paramCount} param{policy.paramCount !== 1 ? 's' : ''} changed + + + + { + e.stopPropagation(); + onPolicyInfoClick(policy.id); + }} + > + + + + + + + ); + })} + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policy/PolicyCreationContent.tsx b/app/src/pages/reportBuilder/modals/policy/PolicyCreationContent.tsx new file mode 100644 index 000000000..5b102edf5 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policy/PolicyCreationContent.tsx @@ -0,0 +1,367 @@ +/** + * PolicyCreationContent - Main content area for policy creation mode (V6 styled) + */ +import { Dispatch, SetStateAction } from 'react'; +import { IconScale, IconTrash } from '@tabler/icons-react'; +import { + ActionIcon, + Badge, + Box, + Button, + Group, + SegmentedControl, + Stack, + Text, + Title, +} from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import HistoricalValues from '@/pathways/report/components/policyParameterSelector/HistoricalValues'; +import { ValueSetterMode } from '@/pathways/report/components/valueSetters'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { PolicyStateProps } from '@/types/pathwayState'; +import { Parameter } from '@/types/subIngredients/parameter'; +import { + ValueInterval, + ValueIntervalCollection, + ValuesList, +} from '@/types/subIngredients/valueInterval'; +import { capitalize } from '@/utils/stringUtils'; +import { FONT_SIZES } from '../../constants'; +import { ValueSetterComponentsV6 } from '../policyCreation/valueSelectors'; + +// Mode selector options for SegmentedControl +const MODE_OPTIONS = [ + { label: 'Default', value: ValueSetterMode.DEFAULT }, + { label: 'Yearly', value: ValueSetterMode.YEARLY }, + { label: 'Date range', value: ValueSetterMode.DATE }, + { label: 'Multi-year', value: ValueSetterMode.MULTI_YEAR }, +]; + +interface PolicyCreationContentProps { + selectedParam: ParameterMetadata | null; + localPolicy: PolicyStateProps; + policyLabel: string; + policyParameters: Parameter[]; + setPolicyParameters: Dispatch>; + minDate: string; + maxDate: string; + intervals: ValueInterval[]; + setIntervals: Dispatch>; + startDate: string; + setStartDate: Dispatch>; + endDate: string; + setEndDate: Dispatch>; + valueSetterMode: ValueSetterMode; + setValueSetterMode: (mode: ValueSetterMode) => void; + onValueSubmit: () => void; + isReadOnly?: boolean; +} + +export function PolicyCreationContent({ + selectedParam, + localPolicy, + policyLabel, + policyParameters, + setPolicyParameters, + minDate, + maxDate, + intervals, + setIntervals, + startDate, + setStartDate, + endDate, + setEndDate, + valueSetterMode, + setValueSetterMode, + onValueSubmit, + isReadOnly = false, +}: PolicyCreationContentProps) { + // Get base and reform values for chart + const getChartValues = () => { + if (!selectedParam) { + return { baseValues: null, reformValues: null }; + } + const baseValues = new ValueIntervalCollection(selectedParam.values as ValuesList); + const reformValues = new ValueIntervalCollection(baseValues); + const paramToChart = policyParameters.find((p) => p.name === selectedParam.parameter); + if (paramToChart && paramToChart.values && paramToChart.values.length > 0) { + const userIntervals = new ValueIntervalCollection(paramToChart.values as ValuesList); + for (const interval of userIntervals.getIntervals()) { + reformValues.addInterval(interval); + } + } + return { baseValues, reformValues }; + }; + + const { baseValues, reformValues } = getChartValues(); + const ValueSetterToRender = ValueSetterComponentsV6[valueSetterMode]; + + // Get changes for the current parameter + const currentParamChanges = selectedParam + ? policyParameters.find((p) => p.name === selectedParam.parameter)?.values || [] + : []; + + // Format a date range for display + const formatPeriod = (interval: ValueInterval): string => { + const start = interval.startDate; + const end = interval.endDate; + if (!end || end === '9999-12-31') { + const year = start.split('-')[0]; + return `${year} onward`; + } + const startYear = start.split('-')[0]; + const endYear = end.split('-')[0]; + if (startYear === endYear) { + return startYear; + } + return `${startYear}-${endYear}`; + }; + + // Format a value for display + const formatValue = (value: number | string | boolean): string => { + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No'; + } + if (typeof value === 'number') { + if (selectedParam?.unit === '/1') { + return `${(value * 100).toFixed(1)}%`; + } + return value.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0, + }); + } + return String(value); + }; + + // Remove a change from the current parameter + const handleRemoveChange = (indexToRemove: number) => { + if (!selectedParam) { + return; + } + const updatedParameters = policyParameters + .map((param) => { + if (param.name === selectedParam.parameter) { + return { + ...param, + values: param.values.filter((_, i) => i !== indexToRemove), + }; + } + return param; + }) + .filter((param) => param.values.length > 0); + setPolicyParameters(updatedParameters); + }; + + if (!selectedParam) { + return ( + + + + + + + {isReadOnly + ? 'Select a parameter from the menu to view its details.' + : 'Select a parameter from the menu to modify its value for your policy reform.'} + + + + ); + } + + return ( + + + {/* Parameter Header Card */} + + + {capitalize(selectedParam.label || 'Label unavailable')} + + {selectedParam.description && ( + + {selectedParam.description} + + )} + + + {/* 50/50 Split Content */} + + {/* Left Column: Setter + Changes */} + + {/* Value Setter Card — hidden in read-only mode */} + {!isReadOnly && ( + + + + Set new value + + + {/* Mode selector - SegmentedControl per V6 mockup */} + { + setIntervals([]); + setValueSetterMode(value as ValueSetterMode); + }} + size="xs" + data={MODE_OPTIONS} + styles={{ + root: { + background: colors.gray[100], + borderRadius: spacing.radius.md, + }, + indicator: { + background: colors.white, + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + }, + }} + /> + + + + + + + )} + + {/* Changes for this parameter */} + {currentParamChanges.length > 0 && ( + + + + Changes for this parameter + + + {currentParamChanges.length} + + + + {currentParamChanges.map((change, i) => ( + + + {formatPeriod(change)} + + + + {formatValue(change.value)} + + {!isReadOnly && ( + handleRemoveChange(i)} + > + + + )} + + + ))} + + + )} + + + {/* Right Column: Historical Values Chart */} + + + + Historical values + + {baseValues && reformValues && ( + + )} + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policy/PolicyDetailsDrawer.tsx b/app/src/pages/reportBuilder/modals/policy/PolicyDetailsDrawer.tsx new file mode 100644 index 000000000..4c507c30a --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policy/PolicyDetailsDrawer.tsx @@ -0,0 +1,160 @@ +/** + * PolicyDetailsDrawer - Sliding panel showing policy parameter details + */ +import { useMemo, useState } from 'react'; +import { IconChevronRight, IconPencil, IconX } from '@tabler/icons-react'; +import { ActionIcon, Box, Button, Group, Stack, Text, Transition } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { ParameterTreeNode } from '@/libs/buildParameterTree'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { Parameter } from '@/types/subIngredients/parameter'; +import { formatPeriod } from '@/utils/dateUtils'; +import { formatLabelParts, getHierarchicalLabelsFromTree } from '@/utils/parameterLabels'; +import { formatParameterValue } from '@/utils/policyTableHelpers'; +import { FONT_SIZES } from '../../constants'; +import { PolicyOverviewContent } from '../policyCreation'; + +interface PolicyDetailsDrawerProps { + policy: { + id: string; + associationId?: string; + label: string; + paramCount: number; + parameters: Parameter[]; + } | null; + parameters: Record; + parameterTree: ParameterTreeNode | null | undefined; + onClose: () => void; + onSelect: () => void; + onEdit?: () => void; +} + +export function PolicyDetailsDrawer({ + policy, + parameters, + parameterTree, + onClose, + onSelect, + onEdit, +}: PolicyDetailsDrawerProps) { + const [hoveredParamName, setHoveredParamName] = useState(null); + + const modifiedParams = useMemo(() => { + if (!policy) return []; + return policy.parameters.map((param) => { + const hierarchicalLabels = getHierarchicalLabelsFromTree(param.name, parameterTree); + const displayLabel = + hierarchicalLabels.length > 0 + ? formatLabelParts(hierarchicalLabels) + : param.name.split('.').pop() || param.name; + const metadata = parameters[param.name]; + const changes = (param.values || []).map((interval) => ({ + period: formatPeriod(interval.startDate, interval.endDate), + value: formatParameterValue(interval.value, metadata?.unit ?? undefined), + })); + return { paramName: param.name, label: displayLabel, changes }; + }); + }, [policy, parameters, parameterTree]); + + return ( + <> + {/* Overlay */} + + {(transitionStyles) => ( + + )} + + + {/* Drawer */} + + {(transitionStyles) => ( + e.stopPropagation()} + > + {policy && ( + <> + + + + Policy details + + + + + + + + {}} + isReadOnly={true} + modificationCount={policy.paramCount} + modifiedParams={modifiedParams} + hoveredParamName={hoveredParamName} + onHoverParam={setHoveredParamName} + onClickParam={() => {}} + /> + + + + + {onEdit && ( + + )} + + + + )} + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policy/PolicyParameterTree.tsx b/app/src/pages/reportBuilder/modals/policy/PolicyParameterTree.tsx new file mode 100644 index 000000000..6a9cc7162 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policy/PolicyParameterTree.tsx @@ -0,0 +1,173 @@ +/** + * PolicyParameterTree - Parameter tree navigation for policy creation mode + * + * When activeTab / onTabChange are provided, renders a "Policy overview" + * menu item above the search box. The item controls the main content + * area — the sidebar itself always shows the parameter search + tree. + */ +import { useCallback, useMemo } from 'react'; +import { IconListDetails, IconSearch } from '@tabler/icons-react'; +import { useSelector } from 'react-redux'; +import { + Autocomplete, + Box, + NavLink, + ScrollArea, + Skeleton, + Stack, + Text, + UnstyledButton, +} from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { selectSearchableParameters } from '@/libs/metadataUtils'; +import { ParameterTreeNode } from '@/types/metadata'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; +import { modalStyles } from '../../styles'; +import type { SidebarTab } from '../policyCreation/types'; + +interface PolicyParameterTreeProps { + parameterTree: ParameterTreeNode | null; + parameters: Record; + metadataLoading: boolean; + selectedParam: ParameterMetadata | null; + expandedMenuItems: Set; + parameterSearch: string; + setParameterSearch: (search: string) => void; + onMenuItemClick: (paramName: string) => void; + onSearchSelect: (paramName: string) => void; + /** Active sidebar tab — when provided, renders tab buttons above search */ + activeTab?: SidebarTab; + /** Called when the user clicks a tab */ + onTabChange?: (tab: SidebarTab) => void; +} + +export function PolicyParameterTree({ + parameterTree, + parameters: _parameters, + metadataLoading, + selectedParam, + expandedMenuItems, + parameterSearch, + setParameterSearch, + onMenuItemClick, + onSearchSelect, + activeTab, + onTabChange, +}: PolicyParameterTreeProps) { + const hasOverview = activeTab !== undefined && onTabChange !== undefined; + const colorConfig = INGREDIENT_COLORS.policy; + + // Get searchable parameters from memoized selector (computed once when metadata loads) + const searchableParameters = useSelector(selectSearchableParameters); + + // Render nested menu recursively + const renderMenuItems = useCallback( + (items: ParameterTreeNode[]): React.ReactNode => { + return items + .filter((item) => !item.name.includes('pycache')) + .map((item) => ( + onMenuItemClick(item.name)} + childrenOffset={16} + color="primary" + style={{ borderRadius: spacing.radius.sm }} + > + {item.children && expandedMenuItems.has(item.name) && renderMenuItems(item.children)} + + )); + }, + [activeTab, selectedParam?.parameter, expandedMenuItems, onMenuItemClick] + ); + + // Memoize the rendered tree + const renderedMenuTree = useMemo(() => { + if (metadataLoading || !parameterTree) { + return null; + } + return renderMenuItems(parameterTree.children || []); + }, [metadataLoading, parameterTree, renderMenuItems]); + + return ( + + {/* Policy overview menu item (optional) */} + {hasOverview && ( + + onTabChange('overview')} + > + + Policy overview + + + )} + + {/* Search + tree — always visible */} + + {!hasOverview && ( + + PARAMETERS + + )} + } + styles={{ + input: { fontSize: FONT_SIZES.small, height: 32, minHeight: 32 }, + dropdown: { maxHeight: 300 }, + option: { fontSize: FONT_SIZES.small, padding: `${spacing.xs} ${spacing.sm}` }, + }} + size="xs" + /> + + + + {metadataLoading || !parameterTree ? ( + + + + + + ) : ( + renderedMenuTree + )} + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policy/PolicyStatusHeader.tsx b/app/src/pages/reportBuilder/modals/policy/PolicyStatusHeader.tsx new file mode 100644 index 000000000..a75f46670 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policy/PolicyStatusHeader.tsx @@ -0,0 +1,93 @@ +/** + * PolicyStatusHeader - Glassmorphic status bar for policy creation mode + */ +import { IconScale } from '@tabler/icons-react'; +import { Box, Group, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { EditableLabel } from '../../components/EditableLabel'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +interface PolicyStatusHeaderProps { + policyLabel: string; + setPolicyLabel: (label: string) => void; + modificationCount: number; +} + +export function PolicyStatusHeader({ + policyLabel, + setPolicyLabel, + modificationCount, +}: PolicyStatusHeaderProps) { + const colorConfig = INGREDIENT_COLORS.policy; + + const dockStyles = { + statusHeader: { + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + borderRadius: spacing.radius.lg, + border: `1px solid ${modificationCount > 0 ? colorConfig.border : colors.border.light}`, + boxShadow: + modificationCount > 0 + ? `0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px ${colorConfig.border}` + : `0 2px 12px ${colors.shadow.light}`, + padding: `${spacing.sm} ${spacing.lg}`, + transition: 'all 0.3s ease', + margin: spacing.md, + marginBottom: 0, + }, + }; + + return ( + + + + + + + + + + + {modificationCount > 0 ? ( + <> + + + {modificationCount} parameter{modificationCount !== 1 ? 's' : ''} modified + + + ) : ( + + No changes yet + + )} + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policy/index.ts b/app/src/pages/reportBuilder/modals/policy/index.ts new file mode 100644 index 000000000..ba38a34f4 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policy/index.ts @@ -0,0 +1,8 @@ +/** + * Policy modal sub-components + */ +export { PolicyStatusHeader } from './PolicyStatusHeader'; +export { PolicyParameterTree } from './PolicyParameterTree'; +export { PolicyCreationContent } from './PolicyCreationContent'; +export { PolicyBrowseContent } from './PolicyBrowseContent'; +export { PolicyDetailsDrawer } from './PolicyDetailsDrawer'; diff --git a/app/src/pages/reportBuilder/modals/policyCreation/ChangesCard.tsx b/app/src/pages/reportBuilder/modals/policyCreation/ChangesCard.tsx new file mode 100644 index 000000000..8bdfbadb6 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/ChangesCard.tsx @@ -0,0 +1,78 @@ +/** + * ChangesCard - Displays list of modified parameters with their changes + */ + +import React from 'react'; +import { Box, Group, Stack, Text, UnstyledButton } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { ChangesCardProps } from './types'; + +export function ChangesCard({ + modifiedParams, + modificationCount, + selectedParamName, + onSelectParam, +}: ChangesCardProps) { + if (modifiedParams.length === 0) { + return null; + } + + return ( + + + + Changes for this parameter + + + {modificationCount} + + + + {modifiedParams.map((param) => ( + onSelectParam(param.paramName)} + style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: spacing.sm, + background: + selectedParamName === param.paramName ? colors.primary[50] : colors.gray[50], + borderRadius: spacing.radius.sm, + width: '100%', + }} + > + + {param.label} + + + {param.changes.map((change, idx) => ( + + {change.period}: {change.value} + + ))} + + + ))} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/EmptyParameterState.tsx b/app/src/pages/reportBuilder/modals/policyCreation/EmptyParameterState.tsx new file mode 100644 index 000000000..d5d7400aa --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/EmptyParameterState.tsx @@ -0,0 +1,48 @@ +/** + * EmptyParameterState - Displayed when no parameter is selected + */ + +import React from 'react'; +import { IconScale } from '@tabler/icons-react'; +import { Box, Stack, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { FONT_SIZES } from '../../constants'; +import { EmptyParameterStateProps } from './types'; + +export function EmptyParameterState({ + message = 'Select a parameter from the menu to modify its value for your policy reform.', +}: EmptyParameterStateProps) { + return ( + + + + + + + {message} + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/HistoricalValuesCard.tsx b/app/src/pages/reportBuilder/modals/policyCreation/HistoricalValuesCard.tsx new file mode 100644 index 000000000..f6b765fe2 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/HistoricalValuesCard.tsx @@ -0,0 +1,45 @@ +/** + * HistoricalValuesCard - Card wrapper for the historical values chart + */ + +import React from 'react'; +import { Box, Stack, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import HistoricalValues from '@/pathways/report/components/policyParameterSelector/HistoricalValues'; +import { FONT_SIZES } from '../../constants'; +import { HistoricalValuesCardProps } from './types'; + +export function HistoricalValuesCard({ + selectedParam, + baseValues, + reformValues, + policyLabel, +}: HistoricalValuesCardProps) { + return ( + + + + Historical values + + {baseValues && reformValues && ( + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/ParameterHeaderCard.tsx b/app/src/pages/reportBuilder/modals/policyCreation/ParameterHeaderCard.tsx new file mode 100644 index 000000000..a138097b6 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/ParameterHeaderCard.tsx @@ -0,0 +1,30 @@ +/** + * ParameterHeaderCard - Displays the selected parameter name and description + */ + +import React from 'react'; +import { Box, Text, Title } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { capitalize } from '@/utils/stringUtils'; +import { FONT_SIZES } from '../../constants'; +import { ParameterHeaderCardProps } from './types'; + +export function ParameterHeaderCard({ label, description }: ParameterHeaderCardProps) { + return ( + + + {capitalize(label || 'Label unavailable')} + + {description && ( + {description} + )} + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/ParameterSidebar.tsx b/app/src/pages/reportBuilder/modals/policyCreation/ParameterSidebar.tsx new file mode 100644 index 000000000..beb3be920 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/ParameterSidebar.tsx @@ -0,0 +1,164 @@ +/** + * ParameterSidebar - Left sidebar with parameter search and tree navigation + * + * When activeTab / onTabChange are provided, renders a "Policy overview" + * menu item above the search box. The item controls the main content + * area — the sidebar itself always shows the parameter search + tree. + */ + +import React, { useCallback, useMemo } from 'react'; +import { IconListDetails, IconSearch } from '@tabler/icons-react'; +import { + Autocomplete, + Box, + NavLink, + ScrollArea, + Skeleton, + Stack, + Text, + UnstyledButton, +} from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { ParameterTreeNode } from '@/types/metadata'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; +import { modalStyles } from '../../styles'; +import { ParameterSidebarProps } from './types'; + +export function ParameterSidebar({ + parameterTree, + metadataLoading, + selectedParam, + expandedMenuItems, + parameterSearch, + searchableParameters, + onSearchChange, + onSearchSelect, + onMenuItemClick, + activeTab, + onTabChange, +}: ParameterSidebarProps) { + const hasOverview = activeTab !== undefined && onTabChange !== undefined; + const colorConfig = INGREDIENT_COLORS.policy; + + // Render nested menu recursively + const renderMenuItems = useCallback( + (items: ParameterTreeNode[]): React.ReactNode => { + return items + .filter((item) => !item.name.includes('pycache')) + .map((item) => ( + onMenuItemClick(item.name)} + childrenOffset={16} + color="primary" + style={{ + borderRadius: spacing.radius.sm, + }} + > + {item.children && expandedMenuItems.has(item.name) && renderMenuItems(item.children)} + + )); + }, + [activeTab, selectedParam?.parameter, expandedMenuItems, onMenuItemClick] + ); + + // Memoize the rendered tree + const renderedMenuTree = useMemo(() => { + if (metadataLoading || !parameterTree) { + return null; + } + return renderMenuItems(parameterTree.children || []); + }, [metadataLoading, parameterTree, renderMenuItems]); + + return ( + + + {/* Policy overview menu item (optional) */} + {hasOverview && ( + + onTabChange('overview')} + > + + Policy overview + + + )} + + {/* Search + tree — always visible */} + + {!hasOverview && ( + + PARAMETERS + + )} + } + styles={{ + input: { + fontSize: FONT_SIZES.small, + height: 32, + minHeight: 32, + }, + dropdown: { + maxHeight: 300, + }, + option: { + fontSize: FONT_SIZES.small, + padding: `${spacing.xs} ${spacing.sm}`, + }, + }} + size="xs" + /> + + + + {metadataLoading || !parameterTree ? ( + + + + + + ) : ( + renderedMenuTree + )} + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/PolicyCreationHeader.tsx b/app/src/pages/reportBuilder/modals/policyCreation/PolicyCreationHeader.tsx new file mode 100644 index 000000000..1562e8268 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/PolicyCreationHeader.tsx @@ -0,0 +1,144 @@ +/** + * PolicyCreationHeader - Modal header with editable policy name, modification count, and close button + */ + +import React from 'react'; +import { IconPencil, IconScale, IconX } from '@tabler/icons-react'; +import { ActionIcon, Box, Group, Text, TextInput } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +export interface PolicyCreationHeaderProps { + policyLabel: string; + isEditingLabel: boolean; + modificationCount: number; + onLabelChange: (label: string) => void; + onEditingChange: (editing: boolean) => void; + onClose: () => void; +} + +export function PolicyCreationHeader({ + policyLabel, + isEditingLabel, + modificationCount, + onLabelChange, + onEditingChange, + onClose, +}: PolicyCreationHeaderProps) { + const colorConfig = INGREDIENT_COLORS.policy; + + return ( + + + {/* Left side: Policy icon and name */} + + {/* Policy icon */} + + + + + {/* Editable policy name */} + + {isEditingLabel ? ( + onLabelChange(e.currentTarget.value)} + onBlur={() => onEditingChange(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onEditingChange(false); + } + if (e.key === 'Escape') { + onEditingChange(false); + } + }} + placeholder="Untitled policy" + autoFocus + size="xs" + style={{ width: 250 }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + border: 'none', + background: 'transparent', + padding: 0, + }, + }} + /> + ) : ( + <> + + {policyLabel || 'Untitled policy'} + + onEditingChange(true)} + style={{ flexShrink: 0 }} + > + + + + )} + + + + {/* Right side: Modification count + Close */} + + {/* Modification count */} + + {modificationCount > 0 ? ( + <> + + + {modificationCount} parameter{modificationCount !== 1 ? 's' : ''} modified + + + ) : ( + + No changes yet + + )} + + + {/* Close button */} + + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/PolicyNameEditor.tsx b/app/src/pages/reportBuilder/modals/policyCreation/PolicyNameEditor.tsx new file mode 100644 index 000000000..e25f2d77a --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/PolicyNameEditor.tsx @@ -0,0 +1,105 @@ +/** + * PolicyNameEditor - Editable policy name at top of parameter setter pane + */ + +import React from 'react'; +import { IconPencil, IconScale } from '@tabler/icons-react'; +import { ActionIcon, Box, Group, Text, TextInput } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +export interface PolicyNameEditorProps { + policyLabel: string; + isEditingLabel: boolean; + onLabelChange: (label: string) => void; + onEditingChange: (editing: boolean) => void; +} + +export function PolicyNameEditor({ + policyLabel, + isEditingLabel, + onLabelChange, + onEditingChange, +}: PolicyNameEditorProps) { + const colorConfig = INGREDIENT_COLORS.policy; + + return ( + + + {/* Policy icon */} + + + + + {/* Editable policy name */} + + {isEditingLabel ? ( + onLabelChange(e.currentTarget.value)} + onBlur={() => onEditingChange(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onEditingChange(false); + } + if (e.key === 'Escape') { + onEditingChange(false); + } + }} + autoFocus + size="sm" + style={{ flex: 1 }} + styles={{ + input: { + fontFamily: typography.fontFamily.primary, + fontWeight: 600, + fontSize: FONT_SIZES.normal, + }, + }} + /> + ) : ( + <> + + {policyLabel || 'New policy'} + + onEditingChange(true)} + > + + + + )} + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/PolicyOverviewContent.tsx b/app/src/pages/reportBuilder/modals/policyCreation/PolicyOverviewContent.tsx new file mode 100644 index 000000000..25ab758f3 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/PolicyOverviewContent.tsx @@ -0,0 +1,248 @@ +/** + * PolicyOverviewContent - Shared overview tab content for policy modals + * + * Renders the policy naming card and parameter modification grid. + * Used by both PolicyCreationModal and PolicyBrowseModal. + */ +import { Fragment } from 'react'; +import { IconScale } from '@tabler/icons-react'; +import { Box, Group, Stack, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { EditableLabel } from '../../components/EditableLabel'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; +import type { ModifiedParam } from './types'; + +interface PolicyOverviewContentProps { + policyLabel: string; + onLabelChange: (label: string) => void; + isReadOnly: boolean; + modificationCount: number; + modifiedParams: ModifiedParam[]; + hoveredParamName: string | null; + onHoverParam: (name: string | null) => void; + onClickParam: (paramName: string) => void; +} + +const colorConfig = INGREDIENT_COLORS.policy; + +export function PolicyOverviewContent({ + policyLabel, + onLabelChange, + isReadOnly, + modificationCount, + modifiedParams, + hoveredParamName, + onHoverParam, + onClickParam, +}: PolicyOverviewContentProps) { + return ( + + {/* Naming card */} + 0 ? colorConfig.border : colors.border.light}`, + boxShadow: + modificationCount > 0 + ? `0 4px 20px ${colorConfig.border}40` + : `0 2px 8px ${colors.shadow.light}`, + padding: `${spacing.sm} ${spacing.lg}`, + transition: 'all 0.3s ease', + }} + > + + + + + {isReadOnly ? ( + + {policyLabel || 'Untitled policy'} + + ) : ( + + )} + + + + {/* Parameter / Period / Value grid */} + {modifiedParams.length === 0 ? ( + + + No parameter changes{isReadOnly ? '' : ' yet'} + + {!isReadOnly && ( + + Select a parameter from the sidebar to start modifying values. + + )} + + ) : ( + + + {/* Column headers */} + + Parameter + + + Period + + + Value + + + {modifiedParams.map((param) => { + const isHovered = hoveredParamName === param.paramName; + const rowHandlers = { + onClick: () => onClickParam(param.paramName), + onMouseEnter: () => onHoverParam(param.paramName), + onMouseLeave: () => onHoverParam(null), + }; + return ( + + + + {param.label} + + + + {param.changes.map((c, i) => ( + + {c.period} + + ))} + + + {param.changes.map((c, i) => ( + + {c.value} + + ))} + + + ); + })} + + + )} + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/ValueSetterCard.tsx b/app/src/pages/reportBuilder/modals/policyCreation/ValueSetterCard.tsx new file mode 100644 index 000000000..5ecf23474 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/ValueSetterCard.tsx @@ -0,0 +1,94 @@ +/** + * ValueSetterCard - Card with mode selector, value inputs, and submit button + * Uses SegmentedControl for mode selection and V6-styled value selectors + */ + +import React from 'react'; +import { Box, Button, SegmentedControl, Stack, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { ValueSetterMode } from '@/pathways/report/components/valueSetters'; +import { FONT_SIZES } from '../../constants'; +import { ValueSetterCardProps } from './types'; +import { ValueSetterComponentsV6 } from './valueSelectors'; + +// Map enum values to display labels +const MODE_OPTIONS = [ + { label: 'Default', value: ValueSetterMode.DEFAULT }, + { label: 'Yearly', value: ValueSetterMode.YEARLY }, + { label: 'Date range', value: ValueSetterMode.DATE }, + { label: 'Multi-year', value: ValueSetterMode.MULTI_YEAR }, +]; + +export function ValueSetterCard({ + selectedParam, + localPolicy, + minDate, + maxDate, + valueSetterMode, + intervals, + startDate, + endDate, + onModeChange, + onIntervalsChange, + onStartDateChange, + onEndDateChange, + onSubmit, +}: ValueSetterCardProps) { + const ValueSetterToRender = ValueSetterComponentsV6[valueSetterMode]; + + return ( + + + + Set new value + + + {/* Mode selector - SegmentedControl per V6 mockup */} + { + onIntervalsChange([]); + onModeChange(value as ValueSetterMode); + }} + size="xs" + data={MODE_OPTIONS} + styles={{ + root: { + background: colors.gray[100], + borderRadius: spacing.radius.md, + }, + indicator: { + background: colors.white, + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + }, + }} + /> + + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/index.ts b/app/src/pages/reportBuilder/modals/policyCreation/index.ts new file mode 100644 index 000000000..b1f439caa --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/index.ts @@ -0,0 +1,25 @@ +/** + * PolicyCreation components barrel export + */ + +export { ParameterSidebar } from './ParameterSidebar'; +export { PolicyOverviewContent } from './PolicyOverviewContent'; +export { PolicyCreationHeader } from './PolicyCreationHeader'; +export { ParameterHeaderCard } from './ParameterHeaderCard'; +export { ValueSetterCard } from './ValueSetterCard'; +export { ChangesCard } from './ChangesCard'; +export { HistoricalValuesCard } from './HistoricalValuesCard'; +export { EmptyParameterState } from './EmptyParameterState'; + +export type { + EditorMode, + ModifiedParam, + SidebarTab, + ParameterSidebarProps, + PolicyCreationHeaderProps, + ParameterHeaderCardProps, + ValueSetterCardProps, + ChangesCardProps, + HistoricalValuesCardProps, + EmptyParameterStateProps, +} from './types'; diff --git a/app/src/pages/reportBuilder/modals/policyCreation/types.ts b/app/src/pages/reportBuilder/modals/policyCreation/types.ts new file mode 100644 index 000000000..7f7ed085d --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/types.ts @@ -0,0 +1,113 @@ +/** + * Shared types for PolicyCreationModal components + */ + +import { Dispatch, SetStateAction } from 'react'; +import { ValueSetterMode } from '@/pathways/report/components/valueSetters'; +import { ParameterTreeNode } from '@/types/metadata'; +import { ParameterMetadata } from '@/types/metadata/parameterMetadata'; +import { PolicyStateProps } from '@/types/pathwayState'; +import { ValueInterval, ValueIntervalCollection } from '@/types/subIngredients/valueInterval'; + +/** Which sidebar tab is active — controls the main content area */ +export type SidebarTab = 'overview' | 'parameters'; + +/** Controls the editor's behavior: create (new), display (read-only), edit (modifying existing) */ +export type EditorMode = 'create' | 'display' | 'edit'; + +/** + * Modified parameter with formatted changes for display + */ +export interface ModifiedParam { + paramName: string; + label: string; + changes: Array<{ + period: string; + value: string; + }>; +} + +/** + * Props for ParameterSidebar component + */ +export interface ParameterSidebarProps { + parameterTree: { children?: ParameterTreeNode[] } | null; + metadataLoading: boolean; + selectedParam: ParameterMetadata | null; + expandedMenuItems: Set; + parameterSearch: string; + searchableParameters: Array<{ value: string; label: string }>; + onSearchChange: (value: string) => void; + onSearchSelect: (paramName: string) => void; + onMenuItemClick: (paramName: string) => void; + /** Active sidebar tab — when provided, renders tab buttons above search */ + activeTab?: SidebarTab; + /** Called when the user clicks a tab */ + onTabChange?: (tab: SidebarTab) => void; +} + +/** + * Props for PolicyCreationHeader component + */ +export interface PolicyCreationHeaderProps { + policyLabel: string; + isEditingLabel: boolean; + modificationCount: number; + onLabelChange: (label: string) => void; + onEditingChange: (editing: boolean) => void; + onClose: () => void; +} + +/** + * Props for ParameterHeaderCard component + */ +export interface ParameterHeaderCardProps { + label: string; + description?: string; +} + +/** + * Props for ValueSetterCard component + */ +export interface ValueSetterCardProps { + selectedParam: ParameterMetadata; + localPolicy: PolicyStateProps; + minDate: string; + maxDate: string; + valueSetterMode: ValueSetterMode; + intervals: ValueInterval[]; + startDate: string; + endDate: string; + onModeChange: (mode: ValueSetterMode) => void; + onIntervalsChange: Dispatch>; + onStartDateChange: Dispatch>; + onEndDateChange: Dispatch>; + onSubmit: () => void; +} + +/** + * Props for ChangesCard component + */ +export interface ChangesCardProps { + modifiedParams: ModifiedParam[]; + modificationCount: number; + selectedParamName?: string; + onSelectParam: (paramName: string) => void; +} + +/** + * Props for HistoricalValuesCard component + */ +export interface HistoricalValuesCardProps { + selectedParam: ParameterMetadata; + baseValues: ValueIntervalCollection | null; + reformValues: ValueIntervalCollection | null; + policyLabel: string; +} + +/** + * Props for EmptyParameterState component + */ +export interface EmptyParameterStateProps { + message?: string; +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/DateValueSelectorV6.tsx b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/DateValueSelectorV6.tsx new file mode 100644 index 000000000..5a28998dc --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/DateValueSelectorV6.tsx @@ -0,0 +1,120 @@ +/** + * DateValueSelectorV6 - V6 styled date range value selector + * Logic copied from original, only layout/styling changed to match V6 mockup + */ + +import dayjs from 'dayjs'; +import { useEffect, useState } from 'react'; +import { Box, Group, Stack, Text } from '@mantine/core'; +import { DatePickerInput } from '@mantine/dates'; +import { colors, spacing } from '@/designTokens'; +import { getDefaultValueForParam } from '@/pathways/report/components/valueSetters/getDefaultValueForParam'; +import { ValueInputBox } from '@/pathways/report/components/valueSetters/ValueInputBox'; +import { ValueSetterProps } from '@/pathways/report/components/valueSetters/ValueSetterProps'; +import { ValueInterval } from '@/types/subIngredients/valueInterval'; +import { fromLocalDateString, toLocalDateString } from '@/utils/dateUtils'; + +export function DateValueSelectorV6(props: ValueSetterProps) { + const { + param, + policy, + setIntervals, + minDate, + maxDate, + startDate, + setStartDate, + endDate, + setEndDate, + } = props; + + // Local state for param value + const [paramValue, setParamValue] = useState( + getDefaultValueForParam(param, policy, startDate) + ); + + // Set endDate to end of year of startDate + useEffect(() => { + if (startDate) { + const endOfYearDate = dayjs(startDate).endOf('year').format('YYYY-MM-DD'); + setEndDate(endOfYearDate); + } + }, [startDate, setEndDate]); + + // Update param value when startDate changes + useEffect(() => { + if (startDate) { + const newValue = getDefaultValueForParam(param, policy, startDate); + setParamValue(newValue); + } + }, [startDate, param, policy]); + + // Update intervals whenever local state changes + useEffect(() => { + if (startDate && endDate) { + const newInterval: ValueInterval = { + startDate, + endDate, + value: paramValue, + }; + setIntervals([newInterval]); + } else { + setIntervals([]); + } + }, [startDate, endDate, paramValue, setIntervals]); + + function handleStartDateChange(value: Date | string | null) { + setStartDate(toLocalDateString(value)); + } + + function handleEndDateChange(value: Date | string | null) { + setEndDate(toLocalDateString(value)); + } + + // V6 Layout: Two rows - date row, then value row + return ( + + {/* First row: From date + To date */} + + + + From + + + + + + To + + + + + + {/* Second row: Value */} + + + Value + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/DefaultValueSelectorV6.tsx b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/DefaultValueSelectorV6.tsx new file mode 100644 index 000000000..174e61872 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/DefaultValueSelectorV6.tsx @@ -0,0 +1,102 @@ +/** + * DefaultValueSelectorV6 - V6 styled default value selector + * Logic copied from original, only layout/styling changed to match V6 mockup + */ + +import { useEffect, useState } from 'react'; +import { Box, Group, Stack, Text } from '@mantine/core'; +import { YearPickerInput } from '@mantine/dates'; +import { FOREVER } from '@/constants'; +import { colors, spacing } from '@/designTokens'; +import { getDefaultValueForParam } from '@/pathways/report/components/valueSetters/getDefaultValueForParam'; +import { ValueInputBox } from '@/pathways/report/components/valueSetters/ValueInputBox'; +import { ValueSetterProps } from '@/pathways/report/components/valueSetters/ValueSetterProps'; +import { ValueInterval } from '@/types/subIngredients/valueInterval'; +import { fromISODateString, toISODateString } from '@/utils/dateUtils'; + +export function DefaultValueSelectorV6(props: ValueSetterProps) { + const { + param, + policy, + setIntervals, + minDate, + maxDate, + startDate, + setStartDate, + endDate, + setEndDate, + } = props; + + // Local state for param value + const [paramValue, setParamValue] = useState( + getDefaultValueForParam(param, policy, startDate) + ); + + // Set endDate to 2100-12-31 for default mode + useEffect(() => { + setEndDate(FOREVER); + }, [setEndDate]); + + // Update param value when startDate changes + useEffect(() => { + if (startDate) { + const newValue = getDefaultValueForParam(param, policy, startDate); + setParamValue(newValue); + } + }, [startDate, param, policy]); + + // Update intervals whenever local state changes + useEffect(() => { + if (startDate && endDate) { + const newInterval: ValueInterval = { + startDate, + endDate, + value: paramValue, + }; + setIntervals([newInterval]); + } else { + setIntervals([]); + } + }, [startDate, endDate, paramValue, setIntervals]); + + function handleStartDateChange(value: Date | string | null) { + setStartDate(toISODateString(value)); + } + + // V6 Layout: Two rows - date row, then value row + return ( + + {/* First row: From year + "onward" */} + + + + From + + + + + onward + + + + {/* Second row: Value */} + + + Value + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/MultiYearValueSelectorV6.tsx b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/MultiYearValueSelectorV6.tsx new file mode 100644 index 000000000..0a0a0c061 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/MultiYearValueSelectorV6.tsx @@ -0,0 +1,116 @@ +/** + * MultiYearValueSelectorV6 - V6 styled multi-year value selector + * Logic copied from original, only layout/styling changed to match V6 mockup + */ + +import { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Group, Stack, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { getTaxYears } from '@/libs/metadataUtils'; +import { getDefaultValueForParam } from '@/pathways/report/components/valueSetters/getDefaultValueForParam'; +import { ValueInputBox } from '@/pathways/report/components/valueSetters/ValueInputBox'; +import { ValueSetterProps } from '@/pathways/report/components/valueSetters/ValueSetterProps'; +import { RootState } from '@/store'; +import { ValueInterval } from '@/types/subIngredients/valueInterval'; + +export function MultiYearValueSelectorV6(props: ValueSetterProps) { + const { param, policy, setIntervals } = props; + + // Get available years from metadata + const availableYears = useSelector(getTaxYears); + const countryId = useSelector((state: RootState) => state.metadata.currentCountry); + + // Country-specific max years configuration + const MAX_YEARS_BY_COUNTRY: Record = { + us: 10, + uk: 5, + }; + + // Generate years from metadata, starting from current year + const generateYears = () => { + const currentYear = new Date().getFullYear(); + const maxYears = MAX_YEARS_BY_COUNTRY[countryId || 'us'] || 10; + + // Filter available years from metadata to only include current year onwards + const futureYears = availableYears + .map((option) => parseInt(option.value, 10)) + .filter((year) => year >= currentYear) + .sort((a, b) => a - b); + + // Take only the configured max years for this country + return futureYears.slice(0, maxYears); + }; + + const years = generateYears(); + + // Get values for each year - check reform first, then baseline + const getInitialYearValues = useMemo(() => { + const initialValues: Record = {}; + years.forEach((year) => { + initialValues[year] = getDefaultValueForParam(param, policy, `${year}-01-01`); + }); + return initialValues; + }, [param, policy]); + + const [yearValues, setYearValues] = useState>(getInitialYearValues); + + // Update intervals whenever yearValues changes + useEffect(() => { + const newIntervals: ValueInterval[] = Object.keys(yearValues).map((year: string) => ({ + startDate: `${year}-01-01`, + endDate: `${year}-12-31`, + value: yearValues[year], + })); + + setIntervals(newIntervals); + }, [yearValues, setIntervals]); + + const handleYearValueChange = (year: number, value: any) => { + setYearValues((prev) => ({ + ...prev, + [year]: value, + })); + }; + + // Split years into two columns + const midpoint = Math.ceil(years.length / 2); + const leftColumn = years.slice(0, midpoint); + const rightColumn = years.slice(midpoint); + + // V6 Layout: Two columns with year labels and inputs + return ( + + + {leftColumn.map((year) => ( + + + {year} + + handleYearValueChange(year, value)} + onSubmit={props.onSubmit} + /> + + ))} + + + {rightColumn.map((year) => ( + + + {year} + + handleYearValueChange(year, value)} + onSubmit={props.onSubmit} + /> + + ))} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/YearlyValueSelectorV6.tsx b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/YearlyValueSelectorV6.tsx new file mode 100644 index 000000000..31b727881 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/YearlyValueSelectorV6.tsx @@ -0,0 +1,124 @@ +/** + * YearlyValueSelectorV6 - V6 styled yearly value selector + * Logic copied from original, only layout/styling changed to match V6 mockup + */ + +import dayjs from 'dayjs'; +import { useEffect, useState } from 'react'; +import { Box, Group, Stack, Text } from '@mantine/core'; +import { YearPickerInput } from '@mantine/dates'; +import { colors, spacing } from '@/designTokens'; +import { getDefaultValueForParam } from '@/pathways/report/components/valueSetters/getDefaultValueForParam'; +import { ValueInputBox } from '@/pathways/report/components/valueSetters/ValueInputBox'; +import { ValueSetterProps } from '@/pathways/report/components/valueSetters/ValueSetterProps'; +import { ValueInterval } from '@/types/subIngredients/valueInterval'; +import { fromISODateString, toISODateString } from '@/utils/dateUtils'; + +export function YearlyValueSelectorV6(props: ValueSetterProps) { + const { + param, + policy, + setIntervals, + minDate, + maxDate, + startDate, + setStartDate, + endDate, + setEndDate, + } = props; + + // Local state for param value + const [paramValue, setParamValue] = useState( + getDefaultValueForParam(param, policy, startDate) + ); + + // Set endDate to end of year of startDate + useEffect(() => { + if (startDate) { + const endOfYearDate = dayjs(startDate).endOf('year').format('YYYY-MM-DD'); + setEndDate(endOfYearDate); + } + }, [startDate, setEndDate]); + + // Update param value when startDate changes + useEffect(() => { + if (startDate) { + const newValue = getDefaultValueForParam(param, policy, startDate); + setParamValue(newValue); + } + }, [startDate, param, policy]); + + // Update intervals whenever local state changes + useEffect(() => { + if (startDate && endDate) { + const newInterval: ValueInterval = { + startDate, + endDate, + value: paramValue, + }; + setIntervals([newInterval]); + } else { + setIntervals([]); + } + }, [startDate, endDate, paramValue, setIntervals]); + + function handleStartDateChange(value: Date | string | null) { + setStartDate(toISODateString(value)); + } + + function handleEndDateChange(value: Date | string | null) { + const isoString = toISODateString(value); + if (isoString) { + const endOfYearDate = dayjs(isoString).endOf('year').format('YYYY-MM-DD'); + setEndDate(endOfYearDate); + } else { + setEndDate(''); + } + } + + // V6 Layout: Two rows - date row, then value row + return ( + + {/* First row: From year + To year */} + + + + From + + + + + + To + + + + + + {/* Second row: Value */} + + + Value + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/index.ts b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/index.ts new file mode 100644 index 000000000..80bd9c15a --- /dev/null +++ b/app/src/pages/reportBuilder/modals/policyCreation/valueSelectors/index.ts @@ -0,0 +1,21 @@ +/** + * V6-styled value selector components + */ + +import { ValueSetterMode } from '@/pathways/report/components/valueSetters'; +import { DateValueSelectorV6 } from './DateValueSelectorV6'; +import { DefaultValueSelectorV6 } from './DefaultValueSelectorV6'; +import { MultiYearValueSelectorV6 } from './MultiYearValueSelectorV6'; +import { YearlyValueSelectorV6 } from './YearlyValueSelectorV6'; + +export { DefaultValueSelectorV6 } from './DefaultValueSelectorV6'; +export { YearlyValueSelectorV6 } from './YearlyValueSelectorV6'; +export { DateValueSelectorV6 } from './DateValueSelectorV6'; +export { MultiYearValueSelectorV6 } from './MultiYearValueSelectorV6'; + +export const ValueSetterComponentsV6 = { + [ValueSetterMode.DEFAULT]: DefaultValueSelectorV6, + [ValueSetterMode.YEARLY]: YearlyValueSelectorV6, + [ValueSetterMode.DATE]: DateValueSelectorV6, + [ValueSetterMode.MULTI_YEAR]: MultiYearValueSelectorV6, +} as const; diff --git a/app/src/pages/reportBuilder/modals/population/HouseholdCreationContent.tsx b/app/src/pages/reportBuilder/modals/population/HouseholdCreationContent.tsx new file mode 100644 index 000000000..da688b135 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/population/HouseholdCreationContent.tsx @@ -0,0 +1,60 @@ +/** + * HouseholdCreationContent - Household creation form wrapper + */ +import { Box, LoadingOverlay, ScrollArea } from '@mantine/core'; +import HouseholdBuilderForm from '@/components/household/HouseholdBuilderForm'; +import { Household } from '@/types/ingredients/Household'; +import { MetadataState } from '@/types/metadata'; + +interface HouseholdCreationContentProps { + householdDraft: Household | null; + metadata: MetadataState; + reportYear: string; + maritalStatus: 'single' | 'married'; + numChildren: number; + basicPersonFields: string[]; + basicNonPersonFields: string[]; + isCreating: boolean; + onChange: (household: Household) => void; + onMaritalStatusChange: (status: 'single' | 'married') => void; + onNumChildrenChange: (count: number) => void; +} + +export function HouseholdCreationContent({ + householdDraft, + metadata, + reportYear, + maritalStatus, + numChildren, + basicPersonFields, + basicNonPersonFields, + isCreating, + onChange, + onMaritalStatusChange, + onNumChildrenChange, +}: HouseholdCreationContentProps) { + if (!householdDraft) { + return null; + } + + return ( + + + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/population/PopulationBrowseContent.tsx b/app/src/pages/reportBuilder/modals/population/PopulationBrowseContent.tsx new file mode 100644 index 000000000..5b3232ef9 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/population/PopulationBrowseContent.tsx @@ -0,0 +1,270 @@ +/** + * PopulationBrowseContent - Browse mode content for population modal + * + * Handles: + * - National selection + * - Region grids (states, districts, etc.) + * - Household list + */ +import { IconChevronRight, IconHome, IconSearch } from '@tabler/icons-react'; +import { + Box, + Group, + Paper, + ScrollArea, + Skeleton, + Stack, + Text, + TextInput, + UnstyledButton, +} from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { RegionOption } from '@/utils/regionStrategies'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; +import { PopulationCategory } from '../../types'; +import { StateDistrictSelector } from './StateDistrictSelector'; +import { StatePlaceSelector } from './StatePlaceSelector'; + +interface HouseholdItem { + id: string; + label: string; + memberCount: number; +} + +interface PopulationBrowseContentProps { + countryId: 'us' | 'uk'; + activeCategory: PopulationCategory; + searchQuery: string; + setSearchQuery: (query: string) => void; + filteredRegions: RegionOption[]; + allDistricts?: RegionOption[]; // Full list of congressional districts for StateDistrictSelector + filteredHouseholds: HouseholdItem[]; + householdsLoading: boolean; + getSectionTitle: () => string; + getItemCount: () => number; + onSelectGeography: (region: RegionOption | null) => void; + onSelectHousehold: (household: HouseholdItem) => void; +} + +export function PopulationBrowseContent({ + countryId: _countryId, + activeCategory, + searchQuery, + setSearchQuery, + filteredRegions, + allDistricts, + filteredHouseholds, + householdsLoading, + getSectionTitle, + getItemCount, + onSelectGeography, + onSelectHousehold, +}: PopulationBrowseContentProps) { + const colorConfig = INGREDIENT_COLORS.population; + + const styles = { + regionGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', + gap: spacing.sm, + }, + regionChip: { + padding: `${spacing.sm} ${spacing.md}`, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + background: colors.white, + cursor: 'pointer', + transition: 'all 0.15s ease', + fontSize: FONT_SIZES.small, + textAlign: 'center' as const, + }, + householdCard: { + padding: spacing.md, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + background: colors.white, + cursor: 'pointer', + transition: 'all 0.15s ease', + }, + }; + + // StateDistrictSelector and PlaceSelector handle their own search and header + const showExternalSearchAndHeader = activeCategory !== 'districts' && activeCategory !== 'places'; + + return ( + + {/* Search Bar - hidden for national and districts (StateDistrictSelector has its own) */} + {showExternalSearchAndHeader && ( + } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + size="sm" + styles={{ + input: { + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + fontSize: FONT_SIZES.small, + '&:focus': { + borderColor: colorConfig.accent, + }, + }, + }} + /> + )} + + {/* Section Header - hidden for national and districts */} + {showExternalSearchAndHeader && ( + + + {getSectionTitle()} + + + {getItemCount()} {getItemCount() === 1 ? 'option' : 'options'} + + + )} + + {/* Content */} + + {activeCategory === 'my-households' ? ( + // Households list + householdsLoading ? ( + + {[1, 2, 3].map((i) => ( + + ))} + + ) : filteredHouseholds.length === 0 ? ( + + + + + + {searchQuery ? 'No households match your search' : 'No households yet'} + + + {searchQuery + ? 'Try adjusting your search terms' + : 'Create a custom household using the button in the sidebar'} + + + ) : ( + + {filteredHouseholds.map((household) => ( + onSelectHousehold(household)} + > + + + + + + + + {household.label} + + + {household.memberCount}{' '} + {household.memberCount === 1 ? 'member' : 'members'} + + + + + + + ))} + + ) + ) : activeCategory === 'districts' && allDistricts ? ( + // Congressional districts - use StateDistrictSelector + + ) : activeCategory === 'places' ? ( + // US Cities - use StatePlaceSelector grouped by state + + ) : // Standard geography grid (states, countries, constituencies, local authorities) + filteredRegions.length === 0 ? ( + + + No regions match your search + + + ) : ( + + {filteredRegions.map((region) => ( + onSelectGeography(region)} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = colorConfig.border; + e.currentTarget.style.background = colorConfig.bg; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = colors.border.light; + e.currentTarget.style.background = colors.white; + }} + > + {region.label} + + ))} + + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/population/PopulationStatusHeader.tsx b/app/src/pages/reportBuilder/modals/population/PopulationStatusHeader.tsx new file mode 100644 index 000000000..7e9a8b986 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/population/PopulationStatusHeader.tsx @@ -0,0 +1,99 @@ +/** + * PopulationStatusHeader - Glassmorphic status bar for household creation mode + */ +import { IconHome } from '@tabler/icons-react'; +import { Box, Group, Text } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { EditableLabel } from '../../components/EditableLabel'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +interface PopulationStatusHeaderProps { + householdLabel: string; + setHouseholdLabel: (label: string) => void; + memberCount: number; +} + +export function PopulationStatusHeader({ + householdLabel, + setHouseholdLabel, + memberCount, +}: PopulationStatusHeaderProps) { + const colorConfig = INGREDIENT_COLORS.population; + + const dockStyles = { + statusHeader: { + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + borderRadius: spacing.radius.lg, + border: `1px solid ${memberCount > 0 ? colorConfig.border : colors.border.light}`, + boxShadow: + memberCount > 0 + ? `0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px ${colorConfig.border}` + : `0 2px 12px ${colors.shadow.light}`, + padding: `${spacing.sm} ${spacing.lg}`, + transition: 'all 0.3s ease', + margin: spacing.md, + marginBottom: 0, + }, + }; + + return ( + + + {/* Left side: Household icon and editable name */} + + {/* Household icon */} + + + + + {/* Editable household name */} + + + + {/* Right side: Member count */} + + + {memberCount > 0 ? ( + <> + + + {memberCount} member{memberCount !== 1 ? 's' : ''} + + + ) : ( + + No members yet + + )} + + + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/population/StateDistrictSelector.tsx b/app/src/pages/reportBuilder/modals/population/StateDistrictSelector.tsx new file mode 100644 index 000000000..353676d57 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/population/StateDistrictSelector.tsx @@ -0,0 +1,338 @@ +/** + * StateDistrictSelector - Congressional district selector grouped by state + * + * Displays states as headers with their congressional districts underneath. + * Districts are shown as ordinal numbers (1st, 2nd, etc.) or "At-large" for + * single-district states. + */ +import { useMemo } from 'react'; +import { IconSearch } from '@tabler/icons-react'; +import { Box, Group, Stack, Text, TextInput, UnstyledButton } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { RegionOption } from '@/utils/regionStrategies'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +// ============================================================================ +// Types +// ============================================================================ + +interface StateDistrictSelectorProps { + districts: RegionOption[]; + searchQuery: string; + setSearchQuery: (query: string) => void; + onSelectDistrict: (district: RegionOption) => void; +} + +interface StateGroup { + stateName: string; + stateAbbreviation: string; + districts: RegionOption[]; +} + +// ============================================================================ +// Pure utility functions +// ============================================================================ + +function formatOrdinal(num: number): string { + const suffixes = ['th', 'st', 'nd', 'rd']; + const v = num % 100; + return num + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]); +} + +function extractDistrictNumber(label: string): number | null { + const match = label.match(/(\d+)/); + return match ? parseInt(match[1], 10) : null; +} + +function sortDistrictsNumerically(districts: RegionOption[]): RegionOption[] { + return [...districts].sort((a, b) => { + const numA = extractDistrictNumber(a.label) || 0; + const numB = extractDistrictNumber(b.label) || 0; + return numA - numB; + }); +} + +function sortGroupsAlphabetically(groups: StateGroup[]): StateGroup[] { + return [...groups].sort((a, b) => a.stateName.localeCompare(b.stateName)); +} + +function groupDistrictsByState(districts: RegionOption[]): StateGroup[] { + const groups = new Map(); + + for (const district of districts) { + const stateName = district.stateName || 'Unknown'; + const stateAbbr = district.stateAbbreviation || ''; + + if (!groups.has(stateName)) { + groups.set(stateName, { + stateName, + stateAbbreviation: stateAbbr, + districts: [], + }); + } + groups.get(stateName)!.districts.push(district); + } + + const sortedGroups = sortGroupsAlphabetically(Array.from(groups.values())); + + return sortedGroups.map((group) => ({ + ...group, + districts: sortDistrictsNumerically(group.districts), + })); +} + +function buildDistrictCountLookup(groups: StateGroup[]): Map { + const counts = new Map(); + for (const group of groups) { + counts.set(group.stateName, group.districts.length); + } + return counts; +} + +function filterGroupsByQuery(groups: StateGroup[], query: string): StateGroup[] { + if (!query.trim()) { + return groups; + } + + const normalizedQuery = query.toLowerCase(); + + return groups + .map((group) => { + const stateMatches = group.stateName.toLowerCase().includes(normalizedQuery); + if (stateMatches) { + return group; + } + + const matchingDistricts = group.districts.filter((d) => + d.label.toLowerCase().includes(normalizedQuery) + ); + + if (matchingDistricts.length > 0) { + return { ...group, districts: matchingDistricts }; + } + + return null; + }) + .filter((group): group is StateGroup => group !== null); +} + +function getDistrictDisplayLabel( + district: RegionOption, + stateName: string, + originalCounts: Map +): string { + const originalCount = originalCounts.get(stateName) || 0; + if (originalCount === 1) { + return 'At-large'; + } + + const num = extractDistrictNumber(district.label); + return num ? formatOrdinal(num) : district.label; +} + +function countTotalDistricts(groups: StateGroup[]): number { + return groups.reduce((sum, group) => sum + group.districts.length, 0); +} + +// ============================================================================ +// Styles +// ============================================================================ + +const colorConfig = INGREDIENT_COLORS.population; + +const styles = { + stateHeader: { + padding: `${spacing.sm} 0`, + borderBottom: `1px solid ${colors.border.light}`, + marginBottom: spacing.sm, + }, + districtGrid: { + display: 'flex', + flexWrap: 'wrap' as const, + gap: spacing.xs, + marginBottom: spacing.lg, + }, + districtChip: { + padding: `${spacing.xs} ${spacing.md}`, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + background: colors.white, + cursor: 'pointer', + transition: 'all 0.15s ease', + fontSize: FONT_SIZES.small, + minWidth: 60, + textAlign: 'center' as const, + }, + emptyState: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + padding: spacing['4xl'], + gap: spacing.md, + }, +}; + +// ============================================================================ +// Sub-components +// ============================================================================ + +function SearchBar({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ( + } + value={value} + onChange={(e) => onChange(e.target.value)} + size="sm" + styles={{ + input: { + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + fontSize: FONT_SIZES.small, + '&:focus': { + borderColor: colorConfig.accent, + }, + }, + }} + /> + ); +} + +function SectionHeader({ count }: { count: number }) { + return ( + + + Congressional districts + + + {count} {count === 1 ? 'district' : 'districts'} + + + ); +} + +function EmptyState() { + return ( + + + No districts match your search + + + ); +} + +function StateHeader({ + stateName, + stateAbbreviation, +}: { + stateName: string; + stateAbbreviation: string; +}) { + return ( + + + {stateName} + {stateAbbreviation && ( + + ({stateAbbreviation}) + + )} + + + ); +} + +function DistrictChip({ label, onClick }: { label: string; onClick: () => void }) { + return ( + { + e.currentTarget.style.borderColor = colorConfig.border; + e.currentTarget.style.background = colorConfig.bg; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = colors.border.light; + e.currentTarget.style.background = colors.white; + }} + > + {label} + + ); +} + +function StateGroupSection({ + group, + originalCounts, + onSelectDistrict, +}: { + group: StateGroup; + originalCounts: Map; + onSelectDistrict: (district: RegionOption) => void; +}) { + return ( + + + + {group.districts.map((district) => ( + onSelectDistrict(district)} + /> + ))} + + + ); +} + +// ============================================================================ +// Main component +// ============================================================================ + +export function StateDistrictSelector({ + districts, + searchQuery, + setSearchQuery, + onSelectDistrict, +}: StateDistrictSelectorProps) { + const stateGroups = useMemo(() => groupDistrictsByState(districts), [districts]); + + const originalDistrictCounts = useMemo( + () => buildDistrictCountLookup(stateGroups), + [stateGroups] + ); + + const filteredGroups = useMemo( + () => filterGroupsByQuery(stateGroups, searchQuery), + [stateGroups, searchQuery] + ); + + const totalDistrictCount = countTotalDistricts(filteredGroups); + + return ( + + + + + {filteredGroups.length === 0 ? ( + + ) : ( + filteredGroups.map((group) => ( + + )) + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/population/StatePlaceSelector.tsx b/app/src/pages/reportBuilder/modals/population/StatePlaceSelector.tsx new file mode 100644 index 000000000..7aa0ee624 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/population/StatePlaceSelector.tsx @@ -0,0 +1,288 @@ +/** + * StatePlaceSelector - City selector grouped by state + * + * Displays states as headers with their cities underneath. + * Cities are sorted alphabetically within each state. + * Mirrors the StateDistrictSelector pattern for consistency. + */ +import { useMemo } from 'react'; +import { IconSearch } from '@tabler/icons-react'; +import { Box, Group, Stack, Text, TextInput, UnstyledButton } from '@mantine/core'; +import { colors, spacing } from '@/designTokens'; +import { getPlaceDisplayName, getUSPlaces, RegionOption } from '@/utils/regionStrategies'; +import { FONT_SIZES, INGREDIENT_COLORS } from '../../constants'; + +// ============================================================================ +// Types +// ============================================================================ + +interface StatePlaceSelectorProps { + searchQuery: string; + setSearchQuery: (query: string) => void; + onSelectPlace: (place: RegionOption) => void; +} + +interface StateGroup { + stateName: string; + stateAbbreviation: string; + places: RegionOption[]; +} + +// ============================================================================ +// Pure utility functions +// ============================================================================ + +function sortPlacesAlphabetically(places: RegionOption[]): RegionOption[] { + return [...places].sort((a, b) => a.label.localeCompare(b.label)); +} + +function sortGroupsAlphabetically(groups: StateGroup[]): StateGroup[] { + return [...groups].sort((a, b) => a.stateName.localeCompare(b.stateName)); +} + +function groupPlacesByState(places: RegionOption[]): StateGroup[] { + const groups = new Map(); + + for (const place of places) { + const stateName = place.stateName || 'Unknown'; + const stateAbbr = place.stateAbbreviation || ''; + + if (!groups.has(stateName)) { + groups.set(stateName, { + stateName, + stateAbbreviation: stateAbbr, + places: [], + }); + } + groups.get(stateName)!.places.push(place); + } + + const sortedGroups = sortGroupsAlphabetically(Array.from(groups.values())); + + return sortedGroups.map((group) => ({ + ...group, + places: sortPlacesAlphabetically(group.places), + })); +} + +function filterGroupsByQuery(groups: StateGroup[], query: string): StateGroup[] { + if (!query.trim()) { + return groups; + } + + const normalizedQuery = query.toLowerCase(); + + return groups + .map((group) => { + const stateMatches = group.stateName.toLowerCase().includes(normalizedQuery); + if (stateMatches) { + return group; + } + + const matchingPlaces = group.places.filter((p) => + p.label.toLowerCase().includes(normalizedQuery) + ); + + if (matchingPlaces.length > 0) { + return { ...group, places: matchingPlaces }; + } + + return null; + }) + .filter((group): group is StateGroup => group !== null); +} + +function countTotalPlaces(groups: StateGroup[]): number { + return groups.reduce((sum, group) => sum + group.places.length, 0); +} + +// ============================================================================ +// Styles +// ============================================================================ + +const colorConfig = INGREDIENT_COLORS.population; + +const styles = { + stateHeader: { + padding: `${spacing.sm} 0`, + borderBottom: `1px solid ${colors.border.light}`, + marginBottom: spacing.sm, + }, + placeGrid: { + display: 'flex', + flexWrap: 'wrap' as const, + gap: spacing.xs, + marginBottom: spacing.lg, + }, + placeChip: { + padding: `${spacing.xs} ${spacing.md}`, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + background: colors.white, + cursor: 'pointer', + transition: 'all 0.15s ease', + fontSize: FONT_SIZES.small, + }, + emptyState: { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + padding: spacing['4xl'], + gap: spacing.md, + }, +}; + +// ============================================================================ +// Sub-components +// ============================================================================ + +function SearchBar({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ( + } + value={value} + onChange={(e) => onChange(e.target.value)} + size="sm" + styles={{ + input: { + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + fontSize: FONT_SIZES.small, + '&:focus': { + borderColor: colorConfig.accent, + }, + }, + }} + /> + ); +} + +function SectionHeader({ count }: { count: number }) { + return ( + + + Cities + + + {count} {count === 1 ? 'city' : 'cities'} + + + ); +} + +function EmptyState() { + return ( + + + No cities match your search + + + ); +} + +function StateHeader({ + stateName, + stateAbbreviation, +}: { + stateName: string; + stateAbbreviation: string; +}) { + return ( + + + {stateName} + {stateAbbreviation && ( + + ({stateAbbreviation}) + + )} + + + ); +} + +function PlaceChip({ label, onClick }: { label: string; onClick: () => void }) { + return ( + { + e.currentTarget.style.borderColor = colorConfig.border; + e.currentTarget.style.background = colorConfig.bg; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = colors.border.light; + e.currentTarget.style.background = colors.white; + }} + > + {label} + + ); +} + +function StateGroupSection({ + group, + onSelectPlace, +}: { + group: StateGroup; + onSelectPlace: (place: RegionOption) => void; +}) { + return ( + + + + {group.places.map((place) => ( + onSelectPlace(place)} + /> + ))} + + + ); +} + +// ============================================================================ +// Main component +// ============================================================================ + +export function StatePlaceSelector({ + searchQuery, + setSearchQuery, + onSelectPlace, +}: StatePlaceSelectorProps) { + // Get all US places as RegionOption array + const allPlaces = useMemo(() => getUSPlaces(), []); + + const stateGroups = useMemo(() => groupPlacesByState(allPlaces), [allPlaces]); + + const filteredGroups = useMemo( + () => filterGroupsByQuery(stateGroups, searchQuery), + [stateGroups, searchQuery] + ); + + const totalPlaceCount = countTotalPlaces(filteredGroups); + + return ( + + + + + {filteredGroups.length === 0 ? ( + + ) : ( + filteredGroups.map((group) => ( + + )) + )} + + + ); +} diff --git a/app/src/pages/reportBuilder/modals/population/index.ts b/app/src/pages/reportBuilder/modals/population/index.ts new file mode 100644 index 000000000..e4b4f6260 --- /dev/null +++ b/app/src/pages/reportBuilder/modals/population/index.ts @@ -0,0 +1,7 @@ +/** + * Population modal sub-components + */ +export { PopulationStatusHeader } from './PopulationStatusHeader'; +export { PopulationBrowseContent } from './PopulationBrowseContent'; +export { HouseholdCreationContent } from './HouseholdCreationContent'; +export { StatePlaceSelector } from './StatePlaceSelector'; diff --git a/app/src/pages/reportBuilder/styles.ts b/app/src/pages/reportBuilder/styles.ts new file mode 100644 index 000000000..27d4bb0ea --- /dev/null +++ b/app/src/pages/reportBuilder/styles.ts @@ -0,0 +1,421 @@ +/** + * Shared styles for ReportBuilder components + */ +import { colors, spacing, typography } from '@/designTokens'; +import { BROWSE_MODAL_CONFIG, FONT_SIZES } from './constants'; + +// ============================================================================ +// PAGE STYLES +// ============================================================================ + +export const pageStyles = { + pageContainer: { + minHeight: '100vh', + background: `linear-gradient(180deg, ${colors.gray[50]} 0%, ${colors.background.secondary} 100%)`, + padding: `${spacing.lg} ${spacing['3xl']}`, + }, + + headerSection: { + marginBottom: spacing.xl, + }, + + mainTitle: { + fontFamily: typography.fontFamily.primary, + fontSize: FONT_SIZES.title, + fontWeight: typography.fontWeight.bold, + color: colors.gray[900], + letterSpacing: '-0.02em', + margin: 0, + }, +}; + +// ============================================================================ +// CANVAS STYLES +// ============================================================================ + +export const canvasStyles = { + canvasContainer: { + background: colors.white, + borderRadius: spacing.radius.xl, + border: `1px solid ${colors.border.light}`, + boxShadow: `0 4px 24px ${colors.shadow.light}`, + padding: spacing['2xl'], + position: 'relative' as const, + overflow: 'hidden', + }, + + canvasGrid: { + background: ` + linear-gradient(90deg, ${colors.gray[100]}18 1px, transparent 1px), + linear-gradient(${colors.gray[100]}18 1px, transparent 1px) + `, + backgroundSize: '20px 20px', + position: 'absolute' as const, + inset: 0, + pointerEvents: 'none' as const, + }, + + simulationsGrid: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: 'auto auto auto', // header, policy, population + gap: `${spacing.sm} ${spacing['2xl']}`, + position: 'relative' as const, + zIndex: 1, + minHeight: '450px', + alignItems: 'start', + }, +}; + +// ============================================================================ +// SIMULATION CARD STYLES +// ============================================================================ + +export const simulationStyles = { + simulationCard: { + background: colors.white, + borderRadius: spacing.radius.lg, + border: `2px solid ${colors.gray[200]}`, + padding: spacing.xl, + transition: 'all 0.2s ease', + position: 'relative' as const, + display: 'grid', + gridRow: 'span 4', // span all 4 rows (header + 3 panels) + gridTemplateRows: 'subgrid', + minWidth: 0, + overflow: 'hidden', + gap: spacing.sm, + }, + + simulationCardActive: { + border: `2px solid ${colors.primary[400]}`, + boxShadow: `0 0 0 4px ${colors.primary[50]}, 0 8px 32px ${colors.shadow.medium}`, + }, + + simulationHeader: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.lg, + minWidth: 0, + overflow: 'hidden', + }, + + simulationTitle: { + fontFamily: typography.fontFamily.primary, + fontSize: FONT_SIZES.normal, + fontWeight: typography.fontWeight.semibold, + color: colors.gray[800], + }, +}; + +// ============================================================================ +// INGREDIENT SECTION STYLES +// ============================================================================ + +export const ingredientStyles = { + ingredientSection: { + padding: spacing.md, + borderRadius: spacing.radius.lg, + border: `1px solid`, + background: 'white', + minWidth: 0, + overflow: 'hidden', + }, + + ingredientSectionHeader: { + display: 'flex', + alignItems: 'center', + gap: spacing.sm, + marginBottom: spacing.md, + }, + + ingredientSectionIcon: { + width: 32, + height: 32, + borderRadius: spacing.radius.md, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, +}; + +// ============================================================================ +// CHIP STYLES +// ============================================================================ + +export const chipStyles = { + // Chip grid for card view (square chips, 3 per row) + chipGridSquare: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: spacing.sm, + }, + + // Row layout for row view + chipRowContainer: { + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.xs, + }, + + // Square chip (expands to fill grid cell, min 80px height) + chipSquare: { + minHeight: 80, + borderRadius: spacing.radius.md, + borderWidth: 1, + borderStyle: 'solid', + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: 6, + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease', + padding: spacing.sm, + }, + + chipSquareSelected: { + borderWidth: 2, + boxShadow: `0 0 0 2px`, + }, + + // Row chip (80 height) + chipRow: { + display: 'flex', + alignItems: 'center', + gap: spacing.md, + padding: `${spacing.md} ${spacing.lg}`, + borderRadius: spacing.radius.md, + borderWidth: 1, + borderStyle: 'solid', + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease', + minHeight: 80, + }, + + chipRowSelected: { + borderWidth: 2, + }, + + chipRowIcon: { + width: 40, + height: 40, + borderRadius: spacing.radius.md, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }, + + // Perforated "Create new policy" chip (expands to fill grid cell) + chipCustomSquare: { + minHeight: 80, + borderRadius: spacing.radius.md, + borderWidth: 2, + borderStyle: 'dashed', + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: 6, + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease', + padding: spacing.sm, + }, + + chipCustomRow: { + display: 'flex', + alignItems: 'center', + gap: spacing.md, + padding: `${spacing.md} ${spacing.lg}`, + borderRadius: spacing.radius.md, + borderWidth: 2, + borderStyle: 'dashed', + cursor: 'pointer', + transition: 'background 0.15s ease, border-color 0.15s ease', + minHeight: 80, + }, +}; + +// ============================================================================ +// ADD SIMULATION CARD STYLES +// ============================================================================ + +export const addSimulationStyles = { + addSimulationCard: { + background: colors.white, + borderRadius: spacing.radius.lg, + border: `2px dashed ${colors.border.medium}`, + padding: spacing.xl, + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: spacing.md, + cursor: 'pointer', + transition: 'all 0.2s ease', + gridRow: 'span 4', // span all 4 rows to match SimulationBlock + }, +}; + +// ============================================================================ +// REPORT META PANEL STYLES +// ============================================================================ + +export const reportMetaStyles = { + reportMetaCard: { + background: colors.white, + borderRadius: spacing.radius.lg, + border: `1px solid ${colors.border.light}`, + padding: `${spacing.xl} ${spacing.xl} ${spacing['2xl']} ${spacing.xl}`, + marginBottom: spacing.xl, + position: 'relative' as const, + overflow: 'hidden', + }, + + inheritedBadge: { + fontSize: FONT_SIZES.tiny, + color: colors.gray[500], + fontStyle: 'italic', + marginLeft: spacing.xs, + }, +}; + +// ============================================================================ +// MODAL STYLES +// ============================================================================ + +export const modalStyles = { + sidebar: { + width: BROWSE_MODAL_CONFIG.sidebarWidth, + borderRight: `1px solid ${colors.border.light}`, + paddingRight: spacing.lg, + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.lg, + flexShrink: 0, + }, + + sidebarSection: { + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.xs, + }, + + sidebarLabel: { + fontSize: FONT_SIZES.tiny, + fontWeight: 600, + color: colors.gray[500], + textTransform: 'uppercase' as const, + letterSpacing: '0.05em', + padding: `0 ${spacing.sm}`, + marginBottom: spacing.xs, + }, + + sidebarItem: { + display: 'flex', + alignItems: 'center', + gap: spacing.sm, + padding: `${spacing.sm} ${spacing.sm}`, + borderRadius: spacing.radius.md, + cursor: 'pointer', + transition: 'all 0.15s ease', + }, + + mainContent: { + flex: 1, + display: 'flex', + flexDirection: 'column' as const, + gap: spacing.lg, + minWidth: 0, + }, + + searchBar: { + marginBottom: spacing.md, + }, + + policyGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', + gap: spacing.md, + }, + + policyCard: { + padding: spacing.lg, + borderRadius: spacing.radius.lg, + border: `1px solid ${colors.border.light}`, + cursor: 'pointer', + transition: 'all 0.15s ease', + background: colors.white, + }, + + householdCard: { + padding: spacing.lg, + borderRadius: spacing.radius.lg, + border: `1px solid ${colors.border.light}`, + cursor: 'pointer', + transition: 'all 0.15s ease', + background: colors.white, + }, + + regionGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', + gap: spacing.sm, + }, + + regionChip: { + padding: `${spacing.sm} ${spacing.md}`, + borderRadius: spacing.radius.md, + border: `1px solid ${colors.border.light}`, + cursor: 'pointer', + transition: 'all 0.15s ease', + background: colors.white, + textAlign: 'left' as const, + }, +}; + +// ============================================================================ +// STATUS HEADER STYLES (DOCK) +// ============================================================================ + +export const statusHeaderStyles = { + container: (hasChanges: boolean, colorConfig: { border: string }) => ({ + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + borderRadius: spacing.radius.lg, + border: `1px solid ${hasChanges ? colorConfig.border : colors.border.light}`, + boxShadow: hasChanges + ? `0 4px 20px ${colorConfig.border}40` + : `0 2px 8px ${colors.shadow.light}`, + padding: `${spacing.sm} ${spacing.lg}`, + marginBottom: spacing.lg, + display: 'flex', + alignItems: 'center', + gap: spacing.md, + transition: 'all 0.3s ease', + }), + + divider: { + width: 1, + height: 24, + background: colors.border.light, + flexShrink: 0, + }, +}; + +// ============================================================================ +// COMBINED STYLES EXPORT (for backwards compatibility) +// ============================================================================ + +export const styles = { + ...pageStyles, + ...canvasStyles, + ...simulationStyles, + ...ingredientStyles, + ...chipStyles, + ...addSimulationStyles, + ...reportMetaStyles, +}; diff --git a/app/src/pages/reportBuilder/types.ts b/app/src/pages/reportBuilder/types.ts new file mode 100644 index 000000000..12f10f491 --- /dev/null +++ b/app/src/pages/reportBuilder/types.ts @@ -0,0 +1,296 @@ +/** + * Type definitions for ReportBuilder components + */ +import { ReactNode } from 'react'; +import { PopulationStateProps, SimulationStateProps } from '@/types/pathwayState'; + +// ============================================================================ +// CORE STATE TYPES +// ============================================================================ + +export interface ReportBuilderState { + id?: string; + label: string | null; + year: string; + simulations: SimulationStateProps[]; +} + +export type IngredientType = 'policy' | 'population' | 'dynamics'; + +export interface IngredientPickerState { + isOpen: boolean; + simulationIndex: number; + ingredientType: IngredientType; +} + +// ============================================================================ +// COLOR CONFIG +// ============================================================================ + +export interface IngredientColorConfig { + icon: string; + bg: string; + border: string; + accent: string; +} + +// ============================================================================ +// DATA TYPES +// ============================================================================ + +export interface SavedPolicy { + id: string; + label: string; + paramCount: number; + createdAt?: string; + updatedAt?: string; +} + +export interface RecentPopulation { + id: string; + label: string; + type: 'geography' | 'household'; + population: PopulationStateProps; +} + +// ============================================================================ +// MODAL TEMPLATE TYPES +// ============================================================================ + +export interface SidebarItem { + id: string; + label: string; + icon: ReactNode; + badge?: string | number; + isActive?: boolean; + onClick: () => void; +} + +export interface BrowseModalSidebarSection { + id: string; + label: string; + items?: SidebarItem[]; +} + +export interface BrowseModalTemplateProps { + isOpen: boolean; + onClose: () => void; + headerIcon: ReactNode; + headerTitle: ReactNode; + headerSubtitle?: string; + /** Content to display on the right side of the header (e.g., status indicator) */ + headerRightContent?: ReactNode; + colorConfig: IngredientColorConfig; + /** Standard sidebar sections - use for simple browse mode sidebars */ + sidebarSections?: BrowseModalSidebarSection[]; + /** Custom sidebar rendering - use when sidebar needs custom layout (e.g., parameter tree) */ + renderSidebar?: () => ReactNode; + /** Sidebar width override (default: 220px) */ + sidebarWidth?: number; + renderMainContent: () => ReactNode; + /** Status header shown above main content (e.g., creation mode status bar) */ + statusHeader?: ReactNode; + /** Footer shown below main content (e.g., creation mode buttons) */ + footer?: ReactNode; + /** Content area padding override (default: spacing.lg). Set to 0 for full-bleed content. */ + contentPadding?: number | string; +} + +// ============================================================================ +// CREATION STATUS HEADER TYPES +// ============================================================================ + +export interface CreationStatusHeaderProps { + colorConfig: IngredientColorConfig; + icon: ReactNode; + label: string; + placeholder: string; + isEditingLabel: boolean; + onLabelChange: (value: string) => void; + onStartEditing: () => void; + onStopEditing: () => void; + statusText: string; + hasChanges: boolean; + children?: ReactNode; +} + +// ============================================================================ +// CHIP COMPONENT TYPES +// ============================================================================ + +export interface OptionChipSquareProps { + icon: ReactNode; + label: string; + description?: string; + isSelected: boolean; + onClick: () => void; + colorConfig: IngredientColorConfig; +} + +export interface OptionChipRowProps { + icon: ReactNode; + label: string; + description?: string; + isSelected: boolean; + onClick: () => void; + colorConfig: IngredientColorConfig; +} + +export interface CreateCustomChipProps { + label: string; + onClick: () => void; + variant: 'square' | 'row'; + colorConfig: IngredientColorConfig; +} + +export interface BrowseMoreChipProps { + label: string; + description?: string; + onClick: () => void; + variant: 'square' | 'row'; + colorConfig: IngredientColorConfig; +} + +// ============================================================================ +// SECTION COMPONENT TYPES +// ============================================================================ + +export interface IngredientSectionProps { + type: IngredientType; + currentId?: string; + countryId?: 'us' | 'uk'; + onQuickSelectPolicy?: () => void; + onSelectSavedPolicy?: (id: string, label: string, paramCount: number) => void; + onQuickSelectPopulation?: (type: 'nationwide') => void; + onSelectRecentPopulation?: (population: PopulationStateProps) => void; + onDeselectPopulation?: () => void; + onDeselectPolicy?: () => void; + onEditPolicy?: () => void; + onCreateCustom: () => void; + onBrowseMore?: () => void; + isInherited?: boolean; + inheritedPopulationType?: 'household' | 'nationwide' | 'subnational' | null; + inheritedPopulationLabel?: string; + savedPolicies?: SavedPolicy[]; + recentPopulations?: RecentPopulation[]; + currentLabel?: string; + isReadOnly?: boolean; + onViewPolicy?: () => void; +} + +export interface SimulationBlockProps { + simulation: SimulationStateProps; + index: number; + countryId: 'us' | 'uk'; + onLabelChange: (label: string) => void; + onQuickSelectPolicy: () => void; + onSelectSavedPolicy: (id: string, label: string, paramCount: number) => void; + onQuickSelectPopulation: () => void; + onSelectRecentPopulation: (population: PopulationStateProps) => void; + onDeselectPolicy: () => void; + onDeselectPopulation: () => void; + onEditPolicy: () => void; + onViewPolicy: () => void; + onCreateCustomPolicy: () => void; + onBrowseMorePolicies: () => void; + onBrowseMorePopulations: () => void; + onRemove?: () => void; + canRemove: boolean; + isRequired?: boolean; + populationInherited?: boolean; + inheritedPopulation?: PopulationStateProps; + savedPolicies: SavedPolicy[]; + recentPopulations: RecentPopulation[]; + isReadOnly?: boolean; +} + +export interface AddSimulationCardProps { + onClick: () => void; + disabled?: boolean; +} + +// ============================================================================ +// MODAL COMPONENT TYPES +// ============================================================================ + +export interface IngredientPickerModalProps { + isOpen: boolean; + onClose: () => void; + type: IngredientType; + onSelect: (value: string) => void; + onCreateNew: () => void; +} + +export interface PolicyBrowseState { + isOpen: boolean; + simulationIndex: number; + initialPolicy?: import('@/types/pathwayState').PolicyStateProps; + initialEditorMode?: 'create' | 'display' | 'edit'; +} + +export interface PolicyBrowseModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (policyId: string, label: string, paramCount: number) => void; + savedPolicies: SavedPolicy[]; + policiesLoading?: boolean; +} + +export interface PopulationBrowseModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (population: PopulationStateProps) => void; + onCreateNew?: () => void; +} + +export interface PolicyCreationModalProps { + isOpen: boolean; + onClose: () => void; + onPolicyCreated: (policyId: string, label: string, paramCount: number) => void; +} + +// ============================================================================ +// TOP BAR TYPES +// ============================================================================ + +export interface TopBarAction { + key: string; + label: string; + icon: ReactNode; + onClick: () => void; + variant: 'primary' | 'secondary'; + disabled?: boolean; + loading?: boolean; + loadingLabel?: string; +} + +// ============================================================================ +// CANVAS AND PAGE TYPES +// ============================================================================ + +export interface SimulationCanvasProps { + reportState: ReportBuilderState; + setReportState: React.Dispatch>; + pickerState: IngredientPickerState; + setPickerState: React.Dispatch>; +} + +export interface ReportMetaPanelProps { + reportState: ReportBuilderState; + setReportState: React.Dispatch>; + isReadOnly?: boolean; +} + +// ============================================================================ +// POPULATION CATEGORIES +// ============================================================================ + +export type PopulationCategory = + | 'frequently-selected' + | 'states' + | 'districts' + | 'places' + | 'countries' + | 'constituencies' + | 'local-authorities' + | 'my-households'; diff --git a/app/src/pages/reportBuilder/utils/hydrateReportBuilderState.ts b/app/src/pages/reportBuilder/utils/hydrateReportBuilderState.ts new file mode 100644 index 000000000..c9eae5eda --- /dev/null +++ b/app/src/pages/reportBuilder/utils/hydrateReportBuilderState.ts @@ -0,0 +1,102 @@ +import type { Geography } from '@/types/ingredients/Geography'; +import type { Household } from '@/types/ingredients/Household'; +import type { Policy } from '@/types/ingredients/Policy'; +import type { Report } from '@/types/ingredients/Report'; +import type { Simulation } from '@/types/ingredients/Simulation'; +import type { UserPolicy } from '@/types/ingredients/UserPolicy'; +import type { + UserGeographyPopulation, + UserHouseholdPopulation, +} from '@/types/ingredients/UserPopulation'; +import type { UserReport } from '@/types/ingredients/UserReport'; +import type { UserSimulation } from '@/types/ingredients/UserSimulation'; +import type { SimulationStateProps } from '@/types/pathwayState'; +import { CURRENT_LAW_LABEL, fromApiPolicyId, isCurrentLaw } from '../currentLaw'; +import type { ReportBuilderState } from '../types'; + +interface HydrateArgs { + userReport: UserReport; + report: Report; + simulations: Simulation[]; + policies: Policy[]; + households: Household[]; + geographies: Geography[]; + userSimulations?: UserSimulation[]; + userPolicies?: UserPolicy[]; + userHouseholds?: UserHouseholdPopulation[]; + userGeographies?: UserGeographyPopulation[]; + currentLawId: number; +} + +export function hydrateReportBuilderState({ + userReport, + report, + simulations, + policies, + households, + geographies, + userSimulations, + userPolicies, + userHouseholds, + currentLawId, +}: HydrateArgs): ReportBuilderState { + const hydratedSimulations: SimulationStateProps[] = simulations.map((sim, index) => { + // Find the user simulation label + const userSim = userSimulations?.find((us) => us.simulationId === sim.id); + const label = userSim?.label || `Simulation #${index + 1}`; + + // Hydrate policy + const resolvedPolicyId = sim.policyId ? fromApiPolicyId(sim.policyId, currentLawId) : undefined; + const policy = policies.find((p) => p.id === sim.policyId); + const userPolicy = userPolicies?.find((up) => up.policyId === sim.policyId); + + const policyState = { + id: resolvedPolicyId, + label: isCurrentLaw(resolvedPolicyId) + ? CURRENT_LAW_LABEL + : userPolicy?.label || policy?.label || null, + parameters: policy?.parameters || [], + }; + + // Hydrate population + let populationState; + if (sim.populationType === 'household') { + const household = households.find((h) => h.id === sim.populationId); + const userHousehold = userHouseholds?.find((uh) => uh.householdId === sim.populationId); + populationState = { + label: userHousehold?.label || null, + type: 'household' as const, + household: household || null, + geography: null, + }; + } else { + const geography = geographies.find( + (g) => g.id === sim.populationId || g.geographyId === sim.populationId + ); + populationState = { + label: geography?.name || null, + type: 'geography' as const, + household: null, + geography: geography || null, + }; + } + + return { + id: sim.id, + label, + countryId: sim.countryId, + apiVersion: sim.apiVersion, + status: sim.status, + output: sim.output, + policy: policyState, + population: populationState, + }; + }); + + return { + id: userReport.id, + label: userReport.label || null, + year: report.year || '', + simulations: hydratedSimulations, + }; +} diff --git a/app/src/pathways/report/ReportPathwayWrapper.tsx b/app/src/pathways/report/ReportPathwayWrapper.tsx deleted file mode 100644 index 46bb2eb2a..000000000 --- a/app/src/pathways/report/ReportPathwayWrapper.tsx +++ /dev/null @@ -1,571 +0,0 @@ -/** - * ReportPathwayWrapper - Pathway orchestrator for report creation - * - * Replaces ReportCreationFlow with local state management. - * Manages all state for report, simulations, policies, and populations. - * - * Phase 3 - Complete implementation with nested simulation/policy/population flows - */ - -import { useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import { ReportAdapter } from '@/adapters'; -import StandardLayout from '@/components/StandardLayout'; -import { MOCK_USER_ID } from '@/constants'; -import { ReportYearProvider } from '@/contexts/ReportYearContext'; -import { useCreateReport } from '@/hooks/useCreateReport'; -import { usePathwayNavigation } from '@/hooks/usePathwayNavigation'; -import { useUserGeographics } from '@/hooks/useUserGeographic'; -import { useUserHouseholds } from '@/hooks/useUserHousehold'; -import { useUserSimulations } from '@/hooks/useUserSimulations'; -import { countryIds } from '@/libs/countries'; -import { RootState } from '@/store'; -import { Report } from '@/types/ingredients/Report'; -import { ReportViewMode } from '@/types/pathwayModes/ReportViewMode'; -import { ReportStateProps, SimulationStateProps } from '@/types/pathwayState'; -import { ReportCreationPayload } from '@/types/payloads'; -import { convertSimulationStateToApi } from '@/utils/ingredientReconstruction'; -import { - createPolicyCallbacks, - createPopulationCallbacks, - createReportCallbacks, - createSimulationCallbacks, -} from '@/utils/pathwayCallbacks'; -import { initializeReportState } from '@/utils/pathwayState/initializeReportState'; -import { getReportOutputPath } from '@/utils/reportRouting'; -import PolicyExistingView from './views/policy/PolicyExistingView'; -// Policy views -import PolicyLabelView from './views/policy/PolicyLabelView'; -import PolicyParameterSelectorView from './views/policy/PolicyParameterSelectorView'; -import PolicySubmitView from './views/policy/PolicySubmitView'; -import GeographicConfirmationView from './views/population/GeographicConfirmationView'; -import HouseholdBuilderView from './views/population/HouseholdBuilderView'; -import PopulationExistingView from './views/population/PopulationExistingView'; -import PopulationLabelView from './views/population/PopulationLabelView'; -// Population views -import PopulationScopeView from './views/population/PopulationScopeView'; -// Report-level views -import ReportLabelView from './views/ReportLabelView'; -import ReportSetupView from './views/ReportSetupView'; -import ReportSimulationExistingView from './views/ReportSimulationExistingView'; -import ReportSimulationSelectionView from './views/ReportSimulationSelectionView'; -import ReportSubmitView from './views/ReportSubmitView'; -// Simulation views -import SimulationLabelView from './views/simulation/SimulationLabelView'; -import SimulationPolicySetupView from './views/simulation/SimulationPolicySetupView'; -import SimulationPopulationSetupView from './views/simulation/SimulationPopulationSetupView'; -import SimulationSetupView from './views/simulation/SimulationSetupView'; -import SimulationSubmitView from './views/simulation/SimulationSubmitView'; - -// View modes that manage their own AppShell (don't need StandardLayout wrapper) -const MODES_WITH_OWN_LAYOUT = new Set([ReportViewMode.POLICY_PARAMETER_SELECTOR]); - -interface ReportPathwayWrapperProps { - onComplete?: () => void; -} - -export default function ReportPathwayWrapper({ onComplete }: ReportPathwayWrapperProps) { - const { countryId: countryIdParam } = useParams<{ countryId: string }>(); - const navigate = useNavigate(); - - // Validate countryId from URL params - if (!countryIdParam) { - return
Error: Country ID not found
; - } - - if (!countryIds.includes(countryIdParam as any)) { - return
Error: Invalid country ID
; - } - - const countryId = countryIdParam as (typeof countryIds)[number]; - - // Initialize report state - const [reportState, setReportState] = useState(() => - initializeReportState(countryId) - ); - const [activeSimulationIndex, setActiveSimulationIndex] = useState<0 | 1>(0); - - const { createReport, isPending: isSubmitting } = useCreateReport(reportState.label || undefined); - - // Get metadata for population views - const metadata = useSelector((state: RootState) => state.metadata); - const currentLawId = useSelector((state: RootState) => state.metadata.currentLawId); - - // ========== NAVIGATION ========== - const { currentMode, navigateToMode, goBack, canGoBack } = usePathwayNavigation( - ReportViewMode.REPORT_LABEL - ); - - // ========== FETCH USER DATA FOR CONDITIONAL NAVIGATION ========== - const userId = MOCK_USER_ID.toString(); - const { data: userSimulations } = useUserSimulations(userId); - const { data: userHouseholds } = useUserHouseholds(userId); - const { data: userGeographics } = useUserGeographics(userId); - - const hasExistingSimulations = (userSimulations?.length ?? 0) > 0; - const hasExistingPopulations = (userHouseholds?.length ?? 0) + (userGeographics?.length ?? 0) > 0; - - // ========== HELPER: Get active simulation ========== - const activeSimulation = reportState.simulations[activeSimulationIndex]; - const otherSimulation = reportState.simulations[activeSimulationIndex === 0 ? 1 : 0]; - - // ========== SHARED CALLBACK FACTORIES ========== - // Report-level callbacks - const reportCallbacks = createReportCallbacks( - setReportState, - navigateToMode, - activeSimulationIndex, - ReportViewMode.REPORT_SELECT_SIMULATION, - ReportViewMode.REPORT_SETUP - ); - - // Policy callbacks for active simulation - const policyCallbacks = createPolicyCallbacks( - setReportState, - (state) => state.simulations[activeSimulationIndex].policy, - (state, policy) => { - const newSimulations = [...state.simulations] as [ - (typeof state.simulations)[0], - (typeof state.simulations)[1], - ]; - newSimulations[activeSimulationIndex].policy = policy; - return { ...state, simulations: newSimulations }; - }, - navigateToMode, - ReportViewMode.SIMULATION_SETUP, - undefined // No onPolicyComplete - stays within report pathway - ); - - // Population callbacks for active simulation - const populationCallbacks = createPopulationCallbacks( - setReportState, - (state) => state.simulations[activeSimulationIndex].population, - (state, population) => { - const newSimulations = [...state.simulations] as [ - (typeof state.simulations)[0], - (typeof state.simulations)[1], - ]; - newSimulations[activeSimulationIndex].population = population; - return { ...state, simulations: newSimulations }; - }, - navigateToMode, - ReportViewMode.SIMULATION_SETUP, - ReportViewMode.POPULATION_LABEL, - undefined // No onPopulationComplete - stays within report pathway - ); - - // Simulation callbacks for active simulation - const simulationCallbacks = createSimulationCallbacks( - setReportState, - (state) => state.simulations[activeSimulationIndex], - (state, simulation) => { - const newSimulations = [...state.simulations] as [ - (typeof state.simulations)[0], - (typeof state.simulations)[1], - ]; - newSimulations[activeSimulationIndex] = simulation; - return { ...state, simulations: newSimulations }; - }, - navigateToMode, - ReportViewMode.REPORT_SETUP, - undefined // No onSimulationComplete - stays within report pathway - ); - - // ========== CUSTOM WRAPPERS FOR SPECIFIC REPORT LOGIC ========== - // Wrapper for navigating to simulation selection (needs to update active index) - // Skips selection view if user has no existing simulations (except for baseline, which has DefaultBaselineOption) - const handleNavigateToSimulationSelection = useCallback( - (simulationIndex: 0 | 1) => { - setActiveSimulationIndex(simulationIndex); - // Always show selection view for baseline (index 0) because it has DefaultBaselineOption - // For reform (index 1), skip if no existing simulations - if (simulationIndex === 0 || hasExistingSimulations) { - reportCallbacks.navigateToSimulationSelection(simulationIndex); - } else { - // Skip selection view, go directly to create new (reform simulation only) - navigateToMode(ReportViewMode.SIMULATION_LABEL); - } - }, - [reportCallbacks, hasExistingSimulations, navigateToMode] - ); - - // Always navigate to policy setup view (shows Current Law, Load Existing, Create New) - const handleNavigateToPolicy = useCallback(() => { - navigateToMode(ReportViewMode.SETUP_POLICY); - }, [navigateToMode]); - - // Conditional navigation to population setup - skip if no existing populations - const handleNavigateToPopulation = useCallback(() => { - if (hasExistingPopulations) { - navigateToMode(ReportViewMode.SETUP_POPULATION); - } else { - // Skip selection view, go directly to create new - navigateToMode(ReportViewMode.POPULATION_SCOPE); - } - }, [hasExistingPopulations, navigateToMode]); - - // Wrapper for current law selection - const handleSelectCurrentLaw = useCallback(() => { - policyCallbacks.handleSelectCurrentLaw(currentLawId, 'Current law'); - }, [currentLawId, policyCallbacks]); - - // Handler for selecting default baseline simulation - // This is called after the simulation has been created by DefaultBaselineOption - const handleSelectDefaultBaseline = useCallback( - (simulationState: SimulationStateProps, _simulationId: string) => { - // Update the active simulation with the created simulation - setReportState((prev) => { - const newSimulations = [...prev.simulations] as [ - (typeof prev.simulations)[0], - (typeof prev.simulations)[1], - ]; - newSimulations[activeSimulationIndex] = simulationState; - return { ...prev, simulations: newSimulations }; - }); - - // Navigate back to report setup - navigateToMode(ReportViewMode.REPORT_SETUP); - }, - [activeSimulationIndex, navigateToMode] - ); - - // ========== REPORT SUBMISSION ========== - const handleSubmitReport = useCallback(() => { - const sim1Id = reportState.simulations[0]?.id; - const sim2Id = reportState.simulations[1]?.id; - - // Validation - if (!sim1Id) { - console.error('[ReportPathwayWrapper] Cannot submit: no baseline simulation'); - return; - } - - // Prepare report data - const reportData: Partial = { - countryId: reportState.countryId, - year: reportState.year, - simulationIds: [sim1Id, sim2Id].filter(Boolean) as string[], - apiVersion: reportState.apiVersion, - }; - - const serializedPayload: ReportCreationPayload = ReportAdapter.toCreationPayload( - reportData as Report - ); - - // Convert SimulationStateProps to Simulation format for CalcOrchestrator - const simulation1Api = convertSimulationStateToApi(reportState.simulations[0]); - const simulation2Api = convertSimulationStateToApi(reportState.simulations[1]); - - if (!simulation1Api) { - console.error('[ReportPathwayWrapper] Failed to convert simulation1 to API format'); - return; - } - - // Submit report - createReport( - { - countryId: reportState.countryId, - payload: serializedPayload, - simulations: { - simulation1: simulation1Api, - simulation2: simulation2Api, - }, - populations: { - household1: reportState.simulations[0].population.household, - household2: reportState.simulations[1]?.population.household, - geography1: reportState.simulations[0].population.geography, - geography2: reportState.simulations[1]?.population.geography, - }, - }, - { - onSuccess: (data) => { - const outputPath = getReportOutputPath(reportState.countryId, data.userReport.id); - navigate(outputPath); - onComplete?.(); - }, - onError: (error) => { - console.error('[ReportPathwayWrapper] Report creation failed:', error); - }, - } - ); - }, [reportState, createReport, navigate, onComplete]); - - // ========== RENDER CURRENT VIEW ========== - // Determine which view to render based on current mode - let currentView: React.ReactNode; - - switch (currentMode) { - // ========== REPORT-LEVEL VIEWS ========== - case ReportViewMode.REPORT_LABEL: - currentView = ( - navigateToMode(ReportViewMode.REPORT_SETUP)} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.REPORT_SETUP: - currentView = ( - navigateToMode(ReportViewMode.REPORT_SUBMIT)} - onPrefillPopulation2={reportCallbacks.prefillPopulation2FromSimulation1} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.REPORT_SELECT_SIMULATION: - currentView = ( - navigateToMode(ReportViewMode.SIMULATION_LABEL)} - onLoadExisting={() => navigateToMode(ReportViewMode.REPORT_SELECT_EXISTING_SIMULATION)} - onSelectDefaultBaseline={handleSelectDefaultBaseline} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.REPORT_SELECT_EXISTING_SIMULATION: - currentView = ( - navigateToMode(ReportViewMode.REPORT_SETUP)} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.REPORT_SUBMIT: - currentView = ( - navigate(`/${countryId}/reports`)} - /> - ); - break; - - // ========== SIMULATION-LEVEL VIEWS ========== - case ReportViewMode.SIMULATION_LABEL: - currentView = ( - navigateToMode(ReportViewMode.SIMULATION_SETUP)} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.SIMULATION_SETUP: - currentView = ( - navigateToMode(ReportViewMode.SIMULATION_SUBMIT)} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.SIMULATION_SUBMIT: - currentView = ( - navigate(`/${countryId}/reports`)} - /> - ); - break; - - // ========== POLICY SETUP COORDINATION ========== - case ReportViewMode.SETUP_POLICY: - currentView = ( - navigateToMode(ReportViewMode.POLICY_LABEL)} - onLoadExisting={() => navigateToMode(ReportViewMode.SELECT_EXISTING_POLICY)} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - // ========== POPULATION SETUP COORDINATION ========== - case ReportViewMode.SETUP_POPULATION: - currentView = ( - navigateToMode(ReportViewMode.POPULATION_SCOPE)} - onLoadExisting={() => navigateToMode(ReportViewMode.SELECT_EXISTING_POPULATION)} - onCopyExisting={reportCallbacks.copyPopulationFromOtherSimulation} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - // ========== POLICY CREATION VIEWS ========== - case ReportViewMode.POLICY_LABEL: - currentView = ( - navigateToMode(ReportViewMode.POLICY_PARAMETER_SELECTOR)} - onBack={canGoBack ? goBack : undefined} - onCancel={() => navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.POLICY_PARAMETER_SELECTOR: - currentView = ( - navigateToMode(ReportViewMode.POLICY_SUBMIT)} - onBack={canGoBack ? goBack : undefined} - /> - ); - break; - - case ReportViewMode.POLICY_SUBMIT: - currentView = ( - navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.SELECT_EXISTING_POLICY: - currentView = ( - navigate(`/${countryId}/reports`)} - /> - ); - break; - - // ========== POPULATION CREATION VIEWS ========== - case ReportViewMode.POPULATION_SCOPE: - currentView = ( - navigate(`/${countryId}/reports`)} - /> - ); - break; - - case ReportViewMode.POPULATION_LABEL: - currentView = ( - { - // Navigate based on population type - if (activeSimulation.population.type === 'household') { - navigateToMode(ReportViewMode.POPULATION_HOUSEHOLD_BUILDER); - } else { - navigateToMode(ReportViewMode.POPULATION_GEOGRAPHIC_CONFIRM); - } - }} - onBack={canGoBack ? goBack : undefined} - /> - ); - break; - - case ReportViewMode.POPULATION_HOUSEHOLD_BUILDER: - currentView = ( - - ); - break; - - case ReportViewMode.POPULATION_GEOGRAPHIC_CONFIRM: - currentView = ( - - ); - break; - - case ReportViewMode.SELECT_EXISTING_POPULATION: - currentView = ( - navigate(`/${countryId}/reports`)} - /> - ); - break; - - default: - currentView =
Unknown view mode: {currentMode}
; - } - - // Conditionally wrap with StandardLayout - // Views in MODES_WITH_OWN_LAYOUT manage their own AppShell - const needsStandardLayout = !MODES_WITH_OWN_LAYOUT.has(currentMode); - - // Wrap with ReportYearProvider so child components can access the year - const wrappedView = ( - {currentView} - ); - - // This is a workaround to allow the param setter to manage its own AppShell - return needsStandardLayout ? {wrappedView} : wrappedView; -} diff --git a/app/src/pathways/report/components/DefaultBaselineOption.tsx b/app/src/pathways/report/components/DefaultBaselineOption.tsx deleted file mode 100644 index fa40a43f1..000000000 --- a/app/src/pathways/report/components/DefaultBaselineOption.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * DefaultBaselineOption - Option card for selecting default baseline simulation - * - * This is a selectable card that renders an option for "Current law + Nationwide population" - * as a quick-select for the baseline simulation in a report. - * - * Unlike other cards, this one doesn't navigate anywhere - it just marks itself as selected - * and the parent view handles creation when "Next" is clicked. - */ - -import { IconChevronRight } from '@tabler/icons-react'; -import { Card, Group, Stack, Text } from '@mantine/core'; -import { spacing } from '@/designTokens'; -import { getDefaultBaselineLabel } from '@/utils/isDefaultBaselineSimulation'; - -interface DefaultBaselineOptionProps { - countryId: string; - isSelected: boolean; - onClick: () => void; -} - -export default function DefaultBaselineOption({ - countryId, - isSelected, - onClick, -}: DefaultBaselineOptionProps) { - const simulationLabel = getDefaultBaselineLabel(countryId); - - return ( - - - - {simulationLabel} - - Use current law with all households nationwide as baseline - - - - - - ); -} diff --git a/app/src/pathways/report/components/policyParameterSelector/HistoricalValues.tsx b/app/src/pathways/report/components/policyParameterSelector/HistoricalValues.tsx index 6d0b04013..779edfa2d 100644 --- a/app/src/pathways/report/components/policyParameterSelector/HistoricalValues.tsx +++ b/app/src/pathways/report/components/policyParameterSelector/HistoricalValues.tsx @@ -44,8 +44,7 @@ export default function PolicyParameterSelectorHistoricalValues( } = props; return ( - - Historical values + {capitalize(param.label)} over time void; + onSubmit?: () => void; } export function ValueInputBox(props: ValueInputBoxProps) { - const { param, value, onChange, label } = props; + const { param, value, onChange, onSubmit, label } = props; // US and UK packages use these type designations inconsistently const USD_UNITS = ['currency-USD', 'currency_USD', 'USD']; @@ -95,6 +96,9 @@ export function ValueInputBox(props: ValueInputBoxProps) { suffix={isPercentage ? '%' : ''} value={displayValue} onChange={handleChange} + onKeyDown={(e) => { + if (e.key === 'Enter') onSubmit?.(); + }} thousandSeparator="," style={{ flex: 1 }} /> diff --git a/app/src/pathways/report/components/valueSetters/ValueSetterProps.ts b/app/src/pathways/report/components/valueSetters/ValueSetterProps.ts index 50d96a96e..f5d50db2f 100644 --- a/app/src/pathways/report/components/valueSetters/ValueSetterProps.ts +++ b/app/src/pathways/report/components/valueSetters/ValueSetterProps.ts @@ -14,4 +14,5 @@ export interface ValueSetterProps { setStartDate: Dispatch>; endDate: string; setEndDate: Dispatch>; + onSubmit?: () => void; } diff --git a/app/src/pathways/report/views/ReportLabelView.tsx b/app/src/pathways/report/views/ReportLabelView.tsx deleted file mode 100644 index 5ba101391..000000000 --- a/app/src/pathways/report/views/ReportLabelView.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { Select, TextInput } from '@mantine/core'; -import PathwayView from '@/components/common/PathwayView'; -import { CURRENT_YEAR } from '@/constants'; -import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -import { getTaxYears } from '@/libs/metadataUtils'; - -interface ReportLabelViewProps { - label: string | null; - year: string | null; - onUpdateLabel: (label: string) => void; - onUpdateYear: (year: string) => void; - onNext: () => void; - onBack?: () => void; - onCancel?: () => void; -} - -export default function ReportLabelView({ - label, - year, - onUpdateLabel, - onUpdateYear, - onNext, - onBack, - onCancel, -}: ReportLabelViewProps) { - const countryId = useCurrentCountry(); - const [localLabel, setLocalLabel] = useState(label || ''); - const [localYear, setLocalYear] = useState(year || CURRENT_YEAR); - - // Get available years from metadata - const availableYears = useSelector(getTaxYears); - - // Use British spelling for UK - const initializeText = countryId === 'uk' ? 'Initialise' : 'Initialize'; - - function handleLocalLabelChange(value: string) { - setLocalLabel(value); - } - - function handleYearChange(value: string | null) { - const newYear = value || CURRENT_YEAR; - setLocalYear(newYear); - } - - function submissionHandler() { - onUpdateLabel(localLabel); - onUpdateYear(localYear); - onNext(); - } - - const formInputs = ( - <> - handleLocalLabelChange(e.currentTarget.value)} - /> -