@telepat/rilo 0.1.0 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ :root{color-scheme:dark;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:15px;--bg: #0d1117;--bg-card: #161b22;--bg-elevated: #1c2330;--border: #30363d;--border-subtle: #21262d;--text: #e6edf3;--text-muted: #7d8590;--text-subtle: #484f58;--accent: #ff4e00;--accent-hover: #ff7a00;--green: #3fb950;--yellow: #d29922;--orange: #db6d28;--red: #f85149;--flame: #ff4e00;--flame-mid: #ff7a00;--flame-yellow: #ffcc00;--sidebar-w: 260px;background:var(--bg);color:var(--text)}*,*:before,*:after{box-sizing:border-box}body{margin:0;height:100dvh;overflow:hidden;background:var(--bg)}button,input,textarea,select{font:inherit;color:inherit}button{cursor:pointer}p,h2,h3{margin:0}textarea,input{border:1px solid var(--border);border-radius:8px;padding:.55rem .75rem;background:var(--bg);color:var(--text);width:100%;resize:vertical;transition:border-color .15s}textarea:focus,input:focus{outline:none;border-color:var(--accent)}.app-shell{display:grid;grid-template-columns:var(--sidebar-w) 1fr;height:100dvh;overflow:hidden}.sidebar{display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg-card);overflow:hidden}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:1.1rem 1rem;border-bottom:1px solid var(--border-subtle);flex-shrink:0}.brand-wordmark{font-size:1.1rem;font-weight:800;letter-spacing:.02em;text-transform:uppercase;color:var(--text);flex-shrink:0}.sidebar-section{flex:1;display:flex;flex-direction:column;gap:.65rem;padding:.9rem;overflow:hidden}.sidebar-row{display:flex;align-items:center;justify-content:space-between}.sidebar-section-title{font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}.project-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;overflow-y:auto;flex:1;min-height:0}.project-item{width:100%;text-align:left;background:transparent;border:1px solid transparent;border-radius:6px;padding:.55rem .75rem;font-size:.88rem;color:var(--text-muted);transition:background .1s,color .1s,border-color .1s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.project-item:hover{background:var(--bg-elevated);color:var(--text)}.project-item-active{background:var(--bg-elevated);border-color:var(--border);border-left-color:var(--accent);color:var(--text);font-weight:500}.sidebar-footer{padding:.6rem .75rem;border-top:1px solid var(--border-subtle);flex-shrink:0}.main-area{display:flex;flex-direction:column;overflow:hidden}.project-header{flex-shrink:0;background:var(--bg-card);border-bottom:1px solid var(--border);padding:1rem 1.5rem 0;display:flex;flex-direction:column;gap:.75rem;z-index:10}.project-header-top{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}.project-title-row{display:flex;align-items:center;gap:.6rem;min-width:0}.project-name{font-size:1.05rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-actions{display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;flex-shrink:0}.step-dots{display:flex;gap:0;align-items:center;overflow-x:auto;padding-bottom:.15rem}.dot{display:flex;align-items:center;gap:.35rem;font-size:.75rem;padding:.2rem .65rem;border-radius:20px;transition:background .2s;white-space:nowrap;flex-shrink:0}.dot:before{content:"";width:7px;height:7px;border-radius:50%;flex-shrink:0}.dot-done{color:var(--green)}.dot-done:before{background:var(--green)}.dot-running{color:var(--yellow);animation:pulse 1.2s ease-in-out infinite}.dot-running:before{background:var(--yellow)}.dot-idle{color:var(--text-muted)}.dot-idle:before{background:var(--border)}.dot-label{font-weight:500}@keyframes pulse{0%,to{opacity:1}50%{opacity:.45}}.tab-spacer{flex:1}.analytics-pane{gap:1.25rem}.analytics-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem}@media(max-width:800px){.analytics-summary{grid-template-columns:repeat(2,1fr)}}.analytics-stat-card{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:10px;padding:1.2rem 1.4rem;display:flex;flex-direction:column;gap:.4rem}.stat-value{font-size:1.5rem;font-weight:600;color:var(--text);line-height:1}.stat-label{font-size:.78rem;color:var(--text-muted)}.stat-ok{color:var(--green)}.stat-fail{color:var(--red)}.analytics-empty{margin-top:.25rem}.analytics-error{color:var(--red)}.analytics-runs{display:flex;flex-direction:column;gap:.35rem}.analytics-runs-title{font-size:.78rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem}.run-row{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden}.run-row-header{display:flex;align-items:center;gap:.75rem;width:100%;background:transparent;border:none;padding:.8rem 1rem;text-align:left;cursor:pointer;transition:background .12s}.run-row-header:hover{background:var(--bg-elevated)}.run-badge{font-size:.72rem;font-weight:600;padding:.15em .55em;border-radius:6px;flex-shrink:0}.run-badge-ok{background:color-mix(in srgb,var(--green) 15%,transparent);color:var(--green)}.run-badge-fail{background:color-mix(in srgb,var(--red) 15%,transparent);color:var(--red)}.run-badge-running{background:color-mix(in srgb,var(--yellow) 15%,transparent);color:var(--yellow)}.run-row-date{font-size:.82rem;color:var(--text);flex:1}.run-row-meta{font-size:.82rem;color:var(--text-muted);flex-shrink:0}.run-row-cost{min-width:5rem;text-align:right}.run-row-chevron{font-size:.65rem;color:var(--text-subtle);flex-shrink:0}.run-stages{border-top:1px solid var(--border-subtle);padding:.75rem 1rem 1rem}.stages-table{width:100%;border-collapse:collapse;font-size:.82rem}.stages-table th{text-align:left;color:var(--text-muted);font-weight:500;padding:.2rem .6rem .4rem;border-bottom:1px solid var(--border-subtle)}.stages-table td{padding:.3rem .6rem;color:var(--text)}.stage-name-cell{display:flex;align-items:center;gap:.45rem}.stage-status-cell{color:var(--text-muted)}.stage-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;display:inline-block}.stage-dot-ok{background:var(--green)}.stage-dot-reused{background:var(--accent)}.stage-dot-fail{background:var(--red)}.stage-dot-running{background:var(--yellow)}.stage-dot-idle{background:var(--border)}.stage-row-pending td{color:var(--text-subtle)}.tab-bar{display:flex;gap:0;border-bottom:none;margin:0 -1.5rem;padding:0 1.5rem;overflow-x:auto}.tab{background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;padding:.7rem 1rem;font-size:.88rem;font-weight:500;color:var(--text-muted);transition:color .15s,border-color .15s;white-space:nowrap;flex-shrink:0;display:flex;align-items:center;gap:.4rem}.tab:hover{color:var(--text)}.tab-active{color:var(--text);border-bottom-color:var(--accent)}.tab-count{font-size:.72rem;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:10px;padding:.05em .45em;color:var(--text-muted);font-weight:500;min-width:1.4em;text-align:center}.tab-content{flex:1;overflow-y:auto;padding:1.5rem}.tab-pane{display:flex;flex-direction:column;gap:2rem}.config-inner-tab-bar{display:flex;gap:0;border-bottom:1px solid var(--border-subtle);margin-bottom:1.5rem;overflow-x:auto}.config-inner-tab{background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;padding:.45rem .85rem;font-size:.82rem;font-weight:500;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s;white-space:nowrap;margin-bottom:-1px}.config-inner-tab:hover{color:var(--text)}.config-inner-tab-active{color:var(--text);border-bottom-color:var(--accent)}.btn{background:var(--bg-elevated);border:1px solid var(--border);border-radius:6px;padding:.45rem .9rem;font-size:.88rem;font-weight:500;color:var(--text);transition:background .15s,border-color .15s,opacity .15s;display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap}.btn:hover:not(:disabled){background:#2d3748;border-color:#4a5568}.btn:disabled{opacity:.45;cursor:default}.btn-sm{padding:.3rem .65rem;font-size:.8rem}.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){background:var(--accent-hover);border-color:var(--accent-hover)}.btn-secondary{background:color-mix(in srgb,var(--accent) 12%,var(--bg-elevated));border-color:color-mix(in srgb,var(--accent) 35%,transparent);color:var(--accent-hover)}.btn-secondary:hover:not(:disabled){background:color-mix(in srgb,var(--accent) 22%,var(--bg-elevated))}.btn-ghost{background:transparent;border-color:transparent}.btn-ghost:hover:not(:disabled){background:var(--bg-elevated);border-color:var(--border)}.btn-danger{color:var(--red)}.btn-danger:hover:not(:disabled){border-color:var(--red)}.full-width{width:100%;justify-content:center}.badge{font-size:.72rem;font-weight:600;padding:.15em .55em;border-radius:10px;border:1px solid currentColor;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0}.badge-ok{color:var(--green)}.badge-running{color:var(--yellow);animation:pulse 1.2s ease-in-out infinite}.badge-pending{color:var(--orange)}.badge-fail{color:var(--red)}.badge-paused{color:var(--yellow)}.pause-banner{font-size:.82rem;color:var(--yellow);background:color-mix(in srgb,var(--yellow) 10%,transparent);border:1px solid color-mix(in srgb,var(--yellow) 30%,transparent);border-radius:6px;padding:.5rem .75rem;margin-bottom:.5rem}.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes toast-in{0%{opacity:0;transform:translateY(.75rem)}to{opacity:1;transform:translateY(0)}}.toast-popup{position:fixed;bottom:1.25rem;right:1.25rem;z-index:200;display:flex;align-items:center;justify-content:space-between;gap:.75rem;min-width:240px;max-width:400px;padding:.65rem .9rem;border-radius:10px;font-size:.84rem;background:var(--bg-elevated);border:1px solid var(--border);box-shadow:0 8px 32px #00000059;animation:toast-in .18s ease-out}.toast-ok{border-left:3px solid var(--green)}.toast-error{border-left:3px solid var(--red)}.toast-close{background:transparent;border:none;padding:0 .2rem;color:var(--text-muted);font-size:.8rem;flex-shrink:0;cursor:pointer}.toast-close:hover{color:var(--text)}.modal-overlay{position:fixed;inset:0;background:#000000a6;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:100;padding:1rem}.modal-panel{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;width:100%;max-width:540px;max-height:90dvh;overflow-y:auto;box-shadow:0 24px 64px #00000080;outline:none}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:1.15rem 1.5rem;border-bottom:1px solid var(--border-subtle)}.modal-title{font-size:1rem;font-weight:600}.modal-close{background:transparent;border:none;font-size:1rem;color:var(--text-muted);padding:.15rem .35rem;border-radius:4px}.modal-close:hover{color:var(--text);background:var(--bg-elevated)}.modal-body{padding:1.5rem}.modal-form{display:flex;flex-direction:column;gap:1rem}.modal-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.field{display:flex;flex-direction:column;gap:.5rem}.field-label{font-size:.82rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.field-hint{font-size:.78rem;color:var(--text-muted);font-weight:400;text-transform:none;letter-spacing:0}.editor-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}.editor-grid .full-width{grid-column:1 / -1}.save-row{display:flex;align-items:center;gap:.75rem}.output-actions-row{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap}.config-grid{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}.config-item{border:1px solid var(--border-subtle);border-radius:10px;background:var(--bg-card);padding:.7rem .8rem;display:flex;flex-direction:column;gap:.3rem}.config-key{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}.config-value{font-size:.95rem;color:var(--text);font-weight:500;word-break:break-word}.config-json{margin:0;border:1px solid var(--border-subtle);border-radius:10px;background:var(--bg-card);padding:.85rem;overflow:auto;font-size:.8rem;line-height:1.45;color:var(--text)}.config-form{display:flex;flex-direction:column;gap:1.5rem;margin-bottom:2rem}.config-form-fields{display:flex;flex-direction:column;gap:1rem}.config-group{border:1px solid var(--border);border-radius:10px;background:color-mix(in srgb,var(--bg-elevated) 65%,transparent);padding:1rem 1.15rem}.config-group-header{margin-bottom:.9rem}.config-group-title{margin:0;font-size:.86rem;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--text)}.config-group-note{margin:.2rem 0 0}.config-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.config-form-field{display:flex;flex-direction:column;gap:.35rem}.config-form-field:has(input[type=checkbox]){flex-direction:row;align-items:center;gap:.5rem}.config-form-field:has(input[type=checkbox]) input[type=checkbox]{width:1rem;height:1rem;flex-shrink:0;cursor:pointer;order:-1}.config-form-field--readonly{justify-content:flex-end;padding:.4rem 0 .1rem}.config-form-label{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}.config-form-optional{font-weight:400;text-transform:none;letter-spacing:0;opacity:.7}.config-select,.config-input{background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:.9rem;padding:.45rem .65rem;width:100%;outline:none;transition:border-color .15s;box-sizing:border-box;font-family:inherit;appearance:auto}.config-select:focus,.config-input:focus{border-color:var(--accent)}.config-form-actions{display:flex;align-items:center;gap:.75rem}.config-color-input-row{display:flex;align-items:center;gap:.5rem}.config-inline-actions{display:flex;gap:.5rem;flex-wrap:wrap}.config-color-picker{width:2.35rem;height:2.1rem;border:1px solid var(--border);border-radius:7px;padding:.1rem;background:var(--bg-elevated);cursor:pointer;flex-shrink:0}@media(max-width:860px){.config-group-grid{grid-template-columns:1fr}}.combo-box{position:relative}.combo-box-list{position:absolute;top:100%;left:0;right:0;z-index:200;margin:2px 0 0;padding:0;list-style:none;background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;box-shadow:0 4px 16px #00000040;max-height:220px;overflow-y:auto}.combo-box-option{padding:.45rem .65rem;font-size:.9rem;color:var(--text);cursor:pointer}.combo-box-option:hover,.combo-box-option--active{background:var(--accent);color:#fff}.model-config-section{border:1px solid var(--border);border-radius:10px;margin-bottom:1.5rem;overflow:hidden}.model-config-toggle{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;gap:.75rem;padding:.9rem 1.15rem;cursor:pointer;background:var(--bg-elevated);box-sizing:border-box;transition:background .15s}.model-config-toggle:hover{background:var(--bg-hover, color-mix(in srgb, var(--bg-elevated) 85%, var(--accent)))}.model-config-summary{display:flex;flex-direction:column;gap:.2rem;min-width:0}.model-config-name{font-size:.92rem;font-weight:600;color:var(--text)}.model-config-url{font-size:.78rem}a.model-config-url{color:var(--accent);text-decoration:none}a.model-config-url:hover{text-decoration:underline}.model-config-pricing{font-size:.78rem}.model-config-chevron{flex-shrink:0;font-size:.72rem;color:var(--text-muted)}.model-config-body{padding:1.15rem 1.25rem;border-top:1px solid var(--border);background:var(--bg)}.model-config-body .config-form{margin-bottom:0}.asset-grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(var(--grid-col-min, 200px),1fr))}.asset-card{border:1px solid var(--border-subtle);border-radius:10px;padding:.65rem;display:flex;flex-direction:column;gap:.6rem;background:var(--bg-card)}.asset-card>.btn:last-child{margin-top:auto}.asset-index{font-size:.72rem;color:var(--text-muted);font-weight:600;padding:0 .15rem}.asset-status-row{display:flex;align-items:center;justify-content:space-between;gap:.45rem}.card-prompt{display:flex;flex-direction:column;gap:.3rem}.card-prompt-header{display:flex;align-items:center;justify-content:space-between}.card-prompt-edit-btn{padding:.1rem .35rem;font-size:.75rem;opacity:.6;flex-shrink:0}.card-prompt-edit-btn:hover:not(:disabled){opacity:1}.card-prompt-row{display:flex;align-items:flex-start;gap:.35rem}.prompt-text{font-size:.78rem;color:var(--text-muted);line-height:1.45;margin:0;flex:1}.prompt-textarea{font-size:.8rem;resize:none;overflow:hidden;min-height:3.5rem}.prompt-edit-actions{display:flex;gap:.4rem}.media-wrap{aspect-ratio:var(--ar, 9/16);width:100%;background:#000;border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center}.media-wrap img,.media-wrap video{width:100%;height:100%;object-fit:cover;display:block}.media-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:var(--bg-elevated)}.media-placeholder-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.45rem;text-align:center;padding:.65rem}.media-placeholder-title{font-size:.76rem;color:var(--text-muted);font-weight:500}.final-video-section{margin-bottom:.5rem}.final-video-wrap{margin:0 auto}.tab-content:has(.tab-pane-output){overflow:hidden;display:flex;flex-direction:column}.tab-pane-output{flex:1;min-height:0;display:flex;flex-direction:column;gap:0}.tab-pane-output .final-video-section{flex:1;min-height:0;margin-bottom:0;display:flex;flex-direction:column}.tab-pane-output .final-video-wrap{flex:1;min-height:0;display:flex;align-items:center;justify-content:center}.tab-pane-output .media-wrap{aspect-ratio:unset;height:100%;width:auto;max-width:100%}.section-label{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin-bottom:.65rem}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.85rem;padding:3.5rem 1.5rem;text-align:center;height:100%;min-height:200px}.empty-state-title{font-size:1.15rem;font-weight:600;color:var(--text)}.empty-state-brand{font-size:clamp(1.8rem,5vw,2.4rem);font-weight:900;letter-spacing:.04em;text-transform:uppercase;color:var(--text);opacity:.9;margin-bottom:.5rem}.muted{color:var(--text-muted)}.size-sm{font-size:.82rem}.size-xs{font-size:.75rem}@media(max-width:900px){:root{--sidebar-w: 200px}.editor-grid{grid-template-columns:1fr}.project-header-top{gap:.5rem}}@media(max-width:640px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}body{overflow:auto}}
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2
+ <defs>
3
+ <linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
4
+ <stop offset="0%" stop-color="#ff3d00"/>
5
+ <stop offset="60%" stop-color="#ff7a00"/>
6
+ <stop offset="100%" stop-color="#ffcc00"/>
7
+ </linearGradient>
8
+ </defs>
9
+
10
+ <!-- Rounded square background -->
11
+ <rect width="32" height="32" rx="7" fill="#111111"/>
12
+
13
+ <!-- Flame: main triangle, centered in 32x32, with ~4px padding -->
14
+ <!-- Points: tip at top-center, base spanning bottom -->
15
+ <polygon points="16,5 26,27 6,27" fill="#ff4e00"/>
16
+ <!-- Inner lighter triangle -->
17
+ <polygon points="16,12 22,27 10,27" fill="#ff7a00"/>
18
+
19
+ </svg>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <title>Rilo</title>
8
+ <script type="module" crossorigin src="/assets/index-BvNWHzrr.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Wcsyfv3q.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,34 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="18 26 197 48" width="394" height="96">
2
+ <defs>
3
+ <linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
4
+ <stop offset="0%" stop-color="#ff3d00"/>
5
+ <stop offset="60%" stop-color="#ff7a00"/>
6
+ <stop offset="100%" stop-color="#ffcc00"/>
7
+ </linearGradient>
8
+
9
+ <filter id="flameGlow" x="-40%" y="-40%" width="180%" height="180%">
10
+ <feGaussianBlur stdDeviation="1.5" result="blur"/>
11
+ <feMerge>
12
+ <feMergeNode in="blur"/>
13
+ <feMergeNode in="SourceGraphic"/>
14
+ </feMerge>
15
+ </filter>
16
+ </defs>
17
+
18
+ <!-- Transparent background -->
19
+
20
+ <!-- Minimal flame — two triangles -->
21
+ <polygon points="44,31 58,68 30,68" fill="#ff4e00"/>
22
+ <polygon points="44,44 53,68 35,68" fill="#ff7a00"/>
23
+
24
+ <!-- Wordmark — white text, orange "fire" -->
25
+ <text
26
+ x="66" y="68"
27
+ font-family="'Helvetica Neue', Helvetica, Arial, sans-serif"
28
+ font-size="52"
29
+ font-weight="200"
30
+ letter-spacing="-1"
31
+ fill="#ffffff"
32
+ >tale<tspan font-weight="600" fill="#ff4e00">fire</tspan></text>
33
+
34
+ </svg>
@@ -0,0 +1,36 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="18 26 197 48" width="394" height="96">
2
+ <defs>
3
+ <linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
4
+ <stop offset="0%" stop-color="#ff3d00"/>
5
+ <stop offset="60%" stop-color="#ff7a00"/>
6
+ <stop offset="100%" stop-color="#ffcc00"/>
7
+ </linearGradient>
8
+
9
+ <filter id="flameGlow" x="-40%" y="-40%" width="180%" height="180%">
10
+ <feGaussianBlur stdDeviation="1.5" result="blur"/>
11
+ <feMerge>
12
+ <feMergeNode in="blur"/>
13
+ <feMergeNode in="SourceGraphic"/>
14
+ </feMerge>
15
+ </filter>
16
+ </defs>
17
+
18
+ <!-- Transparent background -->
19
+
20
+ <!-- Minimal flame — two triangles, clean and simple -->
21
+ <!-- Main triangle -->
22
+ <polygon points="44,31 58,68 30,68" fill="#ff4e00"/>
23
+ <!-- Small inner triangle cutout to suggest flame shape -->
24
+ <polygon points="44,44 53,68 35,68" fill="#ff7a00"/>
25
+
26
+ <!-- Wordmark — original letter spacing -->
27
+ <text
28
+ x="66" y="68"
29
+ font-family="'Helvetica Neue', Helvetica, Arial, sans-serif"
30
+ font-size="52"
31
+ font-weight="200"
32
+ letter-spacing="-1"
33
+ fill="#1a1a1a"
34
+ >tale<tspan font-weight="600" fill="#ff4e00">fire</tspan></text>
35
+
36
+ </svg>
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@telepat/rilo",
3
- "version": "0.1.0",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "rilo": "src/cli/index.js"
7
7
  },
8
8
  "files": [
9
9
  "src/",
10
+ "frontend/dist/",
10
11
  "models/",
11
12
  "index.js",
12
13
  "README.md"
@@ -30,6 +31,7 @@
30
31
  "frontend:lint": "npm --prefix frontend run lint",
31
32
  "frontend:build": "npm --prefix frontend run build",
32
33
  "frontend:preview": "npm --prefix frontend run preview",
34
+ "prepack": "npm run frontend:build",
33
35
  "docs:start": "npm --prefix docs-site run start",
34
36
  "docs:build": "npm --prefix docs-site run build",
35
37
  "docs:serve": "npm --prefix docs-site run serve",
@@ -5,6 +5,32 @@ function unauthorized(res) {
5
5
  res.status(401).json({ error: 'Unauthorized' });
6
6
  }
7
7
 
8
+ function isLoopbackAddress(address) {
9
+ if (!address || typeof address !== 'string') {
10
+ return false;
11
+ }
12
+
13
+ const normalized = address.toLowerCase();
14
+ return normalized === '127.0.0.1'
15
+ || normalized === '::1'
16
+ || normalized === '::ffff:127.0.0.1';
17
+ }
18
+
19
+ function shouldBypassAuth(req, options) {
20
+ if (!options.previewMode) {
21
+ return false;
22
+ }
23
+
24
+ if (options.allowUnauthenticatedExposedPreview) {
25
+ return true;
26
+ }
27
+
28
+ const socketAddress = typeof req?.socket?.remoteAddress === 'string'
29
+ ? req.socket.remoteAddress
30
+ : '';
31
+ return isLoopbackAddress(socketAddress);
32
+ }
33
+
8
34
  function getTokenFromAuthorizationHeader(req) {
9
35
  const authHeader = req.get('authorization');
10
36
  if (!authHeader) {
@@ -51,20 +77,50 @@ export function isAuthorizedApiRequest(req, { allowQueryAccessToken = false } =
51
77
  return isMatchingToken(queryToken);
52
78
  }
53
79
 
54
- export function requireBearerToken(req, res, next) {
55
- if (!isAuthorizedApiRequest(req)) {
56
- unauthorized(res);
57
- return;
80
+ export function createAuthGuards(options = {}) {
81
+ const authOptions = {
82
+ previewMode: Boolean(options.previewMode),
83
+ allowUnauthenticatedExposedPreview: Boolean(options.allowUnauthenticatedExposedPreview)
84
+ };
85
+
86
+ function requireBearerTokenWithOptions(req, res, next) {
87
+ if (shouldBypassAuth(req, authOptions)) {
88
+ next();
89
+ return;
90
+ }
91
+
92
+ if (!isAuthorizedApiRequest(req)) {
93
+ unauthorized(res);
94
+ return;
95
+ }
96
+
97
+ next();
58
98
  }
59
99
 
60
- next();
61
- }
100
+ function requireBearerTokenOrAccessTokenWithOptions(req, res, next) {
101
+ if (shouldBypassAuth(req, authOptions)) {
102
+ next();
103
+ return;
104
+ }
62
105
 
63
- export function requireBearerTokenOrAccessToken(req, res, next) {
64
- if (!isAuthorizedApiRequest(req, { allowQueryAccessToken: true })) {
65
- unauthorized(res);
66
- return;
106
+ if (!isAuthorizedApiRequest(req, { allowQueryAccessToken: true })) {
107
+ unauthorized(res);
108
+ return;
109
+ }
110
+
111
+ next();
67
112
  }
68
113
 
69
- next();
114
+ return {
115
+ requireBearerToken: requireBearerTokenWithOptions,
116
+ requireBearerTokenOrAccessToken: requireBearerTokenOrAccessTokenWithOptions
117
+ };
118
+ }
119
+
120
+ export function requireBearerToken(req, res, next) {
121
+ return createAuthGuards().requireBearerToken(req, res, next);
122
+ }
123
+
124
+ export function requireBearerTokenOrAccessToken(req, res, next) {
125
+ return createAuthGuards().requireBearerTokenOrAccessToken(req, res, next);
70
126
  }
@@ -57,7 +57,8 @@ const schemas = {
57
57
  properties: {
58
58
  story: { type: 'string' },
59
59
  project: { type: 'string' },
60
- forceRestart: { type: 'boolean' }
60
+ forceRestart: { type: 'boolean' },
61
+ pauseAfterKeyframes: { type: 'boolean' }
61
62
  },
62
63
  required: ['story'],
63
64
  additionalProperties: true
@@ -180,6 +181,7 @@ const schemas = {
180
181
  type: 'object',
181
182
  properties: {
182
183
  forceRestart: { type: 'boolean' },
184
+ pauseAfterKeyframes: { type: 'boolean' },
183
185
  targetType: {
184
186
  type: 'string',
185
187
  enum: ['script', 'voiceover', 'keyframe', 'segment', 'align', 'burnin']
@@ -7,12 +7,27 @@ export function createJobsRouter() {
7
7
  const router = express.Router();
8
8
 
9
9
  router.post('/', async (req, res) => {
10
- const { story, project, forceRestart } = req.body || {};
10
+ const { story, project, forceRestart, pauseAfterKeyframes } = req.body || {};
11
11
  if (!story) {
12
12
  res.status(400).json({ error: 'story is required' });
13
13
  return;
14
14
  }
15
15
 
16
+ if (typeof story !== 'string') {
17
+ res.status(400).json({ error: 'story must be a string' });
18
+ return;
19
+ }
20
+
21
+ if (forceRestart !== undefined && typeof forceRestart !== 'boolean') {
22
+ res.status(400).json({ error: 'forceRestart must be a boolean when provided' });
23
+ return;
24
+ }
25
+
26
+ if (pauseAfterKeyframes !== undefined && typeof pauseAfterKeyframes !== 'boolean') {
27
+ res.status(400).json({ error: 'pauseAfterKeyframes must be a boolean when provided' });
28
+ return;
29
+ }
30
+
16
31
  const resolvedProject = resolveProjectName(project || `api-${Date.now()}`);
17
32
  const existingActiveJob = findActiveJobByProject(resolvedProject);
18
33
  if (existingActiveJob) {
@@ -28,7 +43,11 @@ export function createJobsRouter() {
28
43
  await writeProjectStory(resolvedProject, story);
29
44
 
30
45
  const job = createJob({ story, project: resolvedProject });
31
- setImmediate(() => runPipeline(job.id, { forceRestart: Boolean(forceRestart) }));
46
+ const pipelineOptions = { forceRestart: forceRestart === true };
47
+ if (pauseAfterKeyframes !== undefined) {
48
+ pipelineOptions.pauseAfterKeyframes = pauseAfterKeyframes;
49
+ }
50
+ setImmediate(() => runPipeline(job.id, pipelineOptions));
32
51
  res.status(202).json({ jobId: job.id, status: job.status });
33
52
  });
34
53
 
@@ -578,6 +578,7 @@ export function createProjectsRouter(deps = {}) {
578
578
 
579
579
  const {
580
580
  forceRestart,
581
+ pauseAfterKeyframes,
581
582
  targetType,
582
583
  index
583
584
  } = req.body || {};
@@ -597,6 +598,11 @@ export function createProjectsRouter(deps = {}) {
597
598
  return;
598
599
  }
599
600
 
601
+ if (pauseAfterKeyframes !== undefined) {
602
+ res.status(400).json({ error: 'pauseAfterKeyframes is not supported for targeted regeneration' });
603
+ return;
604
+ }
605
+
600
606
  const targetRequiresIndex = normalizedTargetType === 'keyframe' || normalizedTargetType === 'segment';
601
607
  if (targetRequiresIndex && !hasIndex) {
602
608
  res.status(400).json({ error: 'index is required for keyframe/segment targeted regeneration' });
@@ -628,9 +634,18 @@ export function createProjectsRouter(deps = {}) {
628
634
  return;
629
635
  }
630
636
 
637
+ if (pauseAfterKeyframes !== undefined && typeof pauseAfterKeyframes !== 'boolean') {
638
+ res.status(400).json({ error: 'pauseAfterKeyframes must be a boolean when provided' });
639
+ return;
640
+ }
641
+
631
642
  const story = await readProjectStory(project);
632
643
  const job = createJobFn({ story, project });
633
- setImmediate(() => runPipelineFn(job.id, { forceRestart: Boolean(forceRestart) }));
644
+ const pipelineOptions = { forceRestart: forceRestart === true };
645
+ if (pauseAfterKeyframes !== undefined) {
646
+ pipelineOptions.pauseAfterKeyframes = pauseAfterKeyframes;
647
+ }
648
+ setImmediate(() => runPipelineFn(job.id, pipelineOptions));
634
649
 
635
650
  res.status(202).json({
636
651
  jobId: job.id,
package/src/api/server.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import express from 'express';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { pathToFileURL } from 'node:url';
3
5
  import { assertRequiredApiEnv, env } from '../config/env.js';
4
6
  import { createJobsRouter } from './routes/jobs.js';
5
7
  import { createProjectAssetsRouter } from './routes/projectAssets.js';
6
8
  import { createProjectsRouter } from './routes/projects.js';
7
9
  import { createWebhookRouter } from './routes/webhooks.js';
8
- import { requireBearerToken, requireBearerTokenOrAccessToken } from './middleware/auth.js';
10
+ import { createAuthGuards } from './middleware/auth.js';
9
11
  import { buildOpenApiSpec } from './openapi/spec.js';
10
12
 
11
13
  const swaggerUiHtml = `<!doctype html>
@@ -42,11 +44,29 @@ function getRequestBaseUrl(req, { fallbackPort = 3000 } = {}) {
42
44
  return `http://localhost:${fallbackPort}`;
43
45
  }
44
46
 
45
- export function createApiApp({ baseUrl } = {}) {
46
- assertRequiredApiEnv();
47
+ function hasDashboardBundle(dashboardDir) {
48
+ if (!dashboardDir || typeof dashboardDir !== 'string') {
49
+ return false;
50
+ }
51
+
52
+ return fs.existsSync(path.join(dashboardDir, 'index.html'));
53
+ }
54
+
55
+ export function createApiApp({ baseUrl, dashboardDir, auth } = {}) {
56
+ const authOptions = {
57
+ previewMode: Boolean(auth?.previewMode),
58
+ allowUnauthenticatedExposedPreview: Boolean(auth?.allowUnauthenticatedExposedPreview)
59
+ };
60
+ const shouldRequireApiToken = !authOptions.previewMode;
61
+ if (shouldRequireApiToken) {
62
+ assertRequiredApiEnv();
63
+ }
64
+
65
+ const guards = createAuthGuards(authOptions);
66
+ const dashboardEnabled = hasDashboardBundle(dashboardDir);
47
67
 
48
68
  const app = express();
49
- app.set('trust proxy', true);
69
+ app.set('trust proxy', !authOptions.previewMode);
50
70
  app.use(express.json({ limit: '2mb' }));
51
71
 
52
72
  app.get('/health', (_req, res) => {
@@ -67,16 +87,30 @@ export function createApiApp({ baseUrl } = {}) {
67
87
  });
68
88
 
69
89
  app.use('/webhooks', createWebhookRouter());
70
- app.use('/projects', requireBearerTokenOrAccessToken, createProjectAssetsRouter());
71
- app.use(requireBearerToken);
90
+ app.use('/projects', guards.requireBearerTokenOrAccessToken, createProjectAssetsRouter());
91
+ app.use(guards.requireBearerToken);
72
92
  app.use('/jobs', createJobsRouter());
73
93
  app.use('/projects', createProjectsRouter());
74
94
 
95
+ if (dashboardEnabled) {
96
+ app.use(express.static(dashboardDir));
97
+ app.get('/', (_req, res) => {
98
+ res.sendFile(path.join(dashboardDir, 'index.html'));
99
+ });
100
+ }
101
+
75
102
  return app;
76
103
  }
77
104
 
78
- export function startApiServer({ port = env.port, baseUrl } = {}) {
79
- const app = createApiApp({ baseUrl });
105
+ export function startApiServer({ port = env.port, host, baseUrl, dashboardDir, auth } = {}) {
106
+ const app = createApiApp({ baseUrl, dashboardDir, auth });
107
+
108
+ if (host) {
109
+ return app.listen(port, host, () => {
110
+ console.log(`rilo api listening on ${host}:${port}`);
111
+ });
112
+ }
113
+
80
114
  return app.listen(port, () => {
81
115
  console.log(`rilo api listening on :${port}`);
82
116
  });
@@ -0,0 +1,126 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { spawn } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import { startApiServer } from '../../api/server.js';
6
+ import { openPath } from './openHome.js';
7
+
8
+ function parsePort(value, fallback) {
9
+ if (!value) {
10
+ return fallback;
11
+ }
12
+
13
+ const parsed = Number(value);
14
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
15
+ throw new Error(`Invalid port: ${value}`);
16
+ }
17
+
18
+ return parsed;
19
+ }
20
+
21
+ function resolveDashboardDir() {
22
+ const commandDir = path.dirname(fileURLToPath(import.meta.url));
23
+ return path.resolve(commandDir, '../../../frontend/dist');
24
+ }
25
+
26
+ function resolveWorkerEntryPoint() {
27
+ const commandDir = path.dirname(fileURLToPath(import.meta.url));
28
+ return path.resolve(commandDir, '../../worker/processor.js');
29
+ }
30
+
31
+ function buildWorkerProcess() {
32
+ const workerPath = resolveWorkerEntryPoint();
33
+ return spawn(process.execPath, [workerPath], {
34
+ stdio: 'inherit'
35
+ });
36
+ }
37
+
38
+ function printUnsafeExposeWarning(url) {
39
+ console.warn('WARNING: running preview in exposed mode without auth.');
40
+ console.warn(`WARNING: dashboard and API are reachable from ${url}`);
41
+ console.warn('WARNING: use only on trusted networks or isolated containers.');
42
+ }
43
+
44
+ function resolveOpenUrl(host, port) {
45
+ if (host === '0.0.0.0' || host === '::') {
46
+ return `http://127.0.0.1:${port}`;
47
+ }
48
+
49
+ return `http://${host}:${port}`;
50
+ }
51
+
52
+ export async function startPreview({ args = process.argv.slice(3) } = {}) {
53
+ const expose = args.includes('--expose');
54
+ const unsafeNoAuth = args.includes('--unsafe-no-auth');
55
+ const noOpen = args.includes('--no-open');
56
+
57
+ if (!expose && unsafeNoAuth) {
58
+ throw new Error('--unsafe-no-auth can only be used with --expose');
59
+ }
60
+
61
+ if (expose && !unsafeNoAuth) {
62
+ throw new Error('Exposed preview requires --unsafe-no-auth');
63
+ }
64
+
65
+ const portArgIndex = args.indexOf('--port');
66
+ const portArg = portArgIndex === -1 ? null : args[portArgIndex + 1] || null;
67
+ const port = parsePort(portArg, 3000);
68
+
69
+ const hostArgIndex = args.indexOf('--host');
70
+ const hostArg = hostArgIndex === -1 ? null : args[hostArgIndex + 1] || null;
71
+ const host = hostArg || (expose ? '0.0.0.0' : '127.0.0.1');
72
+
73
+ const dashboardDir = resolveDashboardDir();
74
+ if (!fs.existsSync(path.join(dashboardDir, 'index.html'))) {
75
+ throw new Error('Dashboard bundle not found. Run `npm run frontend:build` first.');
76
+ }
77
+
78
+ const apiServer = startApiServer({
79
+ port,
80
+ host,
81
+ dashboardDir,
82
+ auth: {
83
+ previewMode: true,
84
+ allowUnauthenticatedExposedPreview: unsafeNoAuth
85
+ }
86
+ });
87
+
88
+ const worker = buildWorkerProcess();
89
+ const openUrl = resolveOpenUrl(host, port);
90
+
91
+ const stopAll = () => {
92
+ if (!worker.killed) {
93
+ worker.kill('SIGTERM');
94
+ }
95
+ apiServer.close();
96
+ };
97
+
98
+ process.once('SIGINT', () => {
99
+ stopAll();
100
+ process.exit(0);
101
+ });
102
+
103
+ process.once('SIGTERM', () => {
104
+ stopAll();
105
+ process.exit(0);
106
+ });
107
+
108
+ worker.once('exit', (code) => {
109
+ apiServer.close(() => {
110
+ if (typeof code === 'number' && code !== 0) {
111
+ process.exit(code);
112
+ }
113
+ });
114
+ });
115
+
116
+ if (unsafeNoAuth) {
117
+ printUnsafeExposeWarning(openUrl);
118
+ }
119
+
120
+ console.log(`Dashboard: ${openUrl}`);
121
+ console.log(`Docs: ${openUrl}/docs`);
122
+
123
+ if (!noOpen) {
124
+ await openPath(openUrl);
125
+ }
126
+ }