@hyperframes/studio 0.4.37 → 0.4.38

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
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-2{bottom:.5rem}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-\[100px\]{left:100px}.left-\[176px\]{left:176px}.left-\[24px\]{left:24px}.left-\[31px\]{left:31px}.left-\[34px\]{left:34px}.left-\[52px\]{left:52px}.left-\[82px\]{left:82px}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-2{top:.5rem}.top-3{top:.75rem}.top-\[18px\]{top:18px}.top-\[21px\]{top:21px}.top-\[27px\]{top:27px}.top-\[3px\]{top:3px}.top-\[51px\]{top:51px}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.z-\[200\]{z-index:200}.z-\[2\]{z-index:2}.z-\[90\]{z-index:90}.z-\[91\]{z-index:91}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[18px\]{height:18px}.h-\[3px\]{height:3px}.h-\[45px\]{height:45px}.h-\[52px\]{height:52px}.h-\[5px\]{height:5px}.h-\[70px\]{height:70px}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-\[70\%\]{max-height:70%}.max-h-\[80vh\]{max-height:80vh}.max-h-full{max-height:100%}.min-h-0{min-height:0px}.min-h-7{min-height:1.75rem}.min-h-8{min-height:2rem}.min-h-9{min-height:2.25rem}.w-0{width:0px}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[110px\]{width:110px}.w-\[118px\]{width:118px}.w-\[160px\]{width:160px}.w-\[320px\]{width:320px}.w-\[56px\]{width:56px}.w-\[58px\]{width:58px}.w-\[72px\]{width:72px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-7{min-width:1.75rem}.min-w-8{min-width:2rem}.min-w-9{min-width:2.25rem}.min-w-\[140px\]{min-width:140px}.min-w-\[160px\]{min-width:160px}.min-w-\[56px\]{min-width:56px}.min-w-\[58px\]{min-width:58px}.min-w-\[96px\]{min-width:96px}.max-w-\[280px\]{max-width:280px}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize-y{resize:vertical}.\!resize{resize:both!important}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-1{row-gap:.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-\[10px\]{border-radius:10px}.rounded-\[11px\]{border-radius:11px}.rounded-\[14px\]{border-radius:14px}.rounded-\[9px\]{border-radius:9px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-green-500\/30{border-color:#22c55e4d}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.border-neutral-700\/40{border-color:#40404066}.border-neutral-700\/50{border-color:#40404080}.border-neutral-700\/60{border-color:#40404099}.border-neutral-800{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.border-neutral-800\/30{border-color:#2626264d}.border-neutral-800\/40{border-color:#26262666}.border-neutral-800\/50{border-color:#26262680}.border-neutral-800\/60{border-color:#26262699}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-700\/50{border-color:#b91c1c80}.border-studio-accent{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.border-studio-accent\/25{border-color:#3ce6ac40}.border-studio-accent\/30{border-color:#3ce6ac4d}.border-studio-accent\/50{border-color:#3ce6ac80}.border-studio-accent\/60{border-color:#3ce6ac99}.border-transparent{border-color:transparent}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-t-white{--tw-border-opacity: 1;border-top-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.bg-\[\#0a0a0b\]{--tw-bg-opacity: 1;background-color:rgb(10 10 11 / var(--tw-bg-opacity, 1))}.bg-\[\#0d1117\]{--tw-bg-opacity: 1;background-color:rgb(13 17 23 / var(--tw-bg-opacity, 1))}.bg-\[\#0f141c\]{--tw-bg-opacity: 1;background-color:rgb(15 20 28 / var(--tw-bg-opacity, 1))}.bg-\[\#3CE6AC\]\/10{background-color:#3ce6ac1a}.bg-\[\#3CE6AC\]\/5{background-color:#3ce6ac0d}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/80{background-color:#000c}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-neutral-600{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity, 1))}.bg-neutral-700\/40{background-color:#40404066}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.bg-neutral-800\/60{background-color:#26262699}.bg-neutral-800\/70{background-color:#262626b3}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.bg-neutral-900\/50{background-color:#17171780}.bg-neutral-900\/95{background-color:#171717f2}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/60{background-color:#7f1d1d99}.bg-red-900\/90{background-color:#7f1d1de6}.bg-red-950\/30{background-color:#450a0a4d}.bg-studio-accent{--tw-bg-opacity: 1;background-color:rgb(60 230 172 / var(--tw-bg-opacity, 1))}.bg-studio-accent\/10{background-color:#3ce6ac1a}.bg-studio-accent\/15{background-color:#3ce6ac26}.bg-studio-accent\/20{background-color:#3ce6ac33}.bg-studio-accent\/\[0\.03\]{background-color:#3ce6ac08}.bg-studio-accent\/\[0\.05\]{background-color:#3ce6ac0d}.bg-studio-accent\/\[0\.06\]{background-color:#3ce6ac0f}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.bg-white\/5{background-color:#ffffff0d}.bg-white\/\[0\.035\]{background-color:#ffffff09}.bg-white\/\[0\.04\]{background-color:#ffffff0a}.bg-white\/\[0\.07\]{background-color:#ffffff12}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0\.5{padding-bottom:.125rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-6{padding-left:1.5rem}.pr-1{padding-right:.25rem}.pr-9{padding-right:2.25rem}.pt-1{padding-top:.25rem}.pt-1\.5{padding-top:.375rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-\[0\.08em\]{letter-spacing:.08em}.tracking-\[0\.16em\]{letter-spacing:.16em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#09090B\]{--tw-text-opacity: 1;color:rgb(9 9 11 / var(--tw-text-opacity, 1))}.text-\[\#7f8796\]{--tw-text-opacity: 1;color:rgb(127 135 150 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity, 1))}.text-neutral-600{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.text-neutral-700{--tw-text-opacity: 1;color:rgb(64 64 64 / var(--tw-text-opacity, 1))}.text-neutral-950{--tw-text-opacity: 1;color:rgb(10 10 10 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-studio-accent{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.text-studio-accent\/50{color:#3ce6ac80}.text-studio-accent\/80{color:#3ce6accc}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/60{color:#fff9}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.accent-studio-accent{accent-color:#3CE6AC}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_18px_40px_rgba\(0\,0\,0\,0\.3\)\,0_4px_14px_rgba\(0\,0\,0\,0\.18\)\]{--tw-shadow: 0 18px 40px rgba(0,0,0,.3),0 4px 14px rgba(0,0,0,.18);--tw-shadow-colored: 0 18px 40px var(--tw-shadow-color), 0 4px 14px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.outline-1{outline-width:1px}.-outline-offset-1{outline-offset:-1px}.outline-\[\#3CE6AC\]\/30{outline-color:#3ce6ac4d}.outline-\[\#3CE6AC\]\/40{outline-color:#3ce6ac66}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-inset{--tw-ring-inset: inset}.ring-studio-accent{--tw-ring-opacity: 1;--tw-ring-color: rgb(60 230 172 / var(--tw-ring-opacity, 1))}.ring-white\/50{--tw-ring-color: rgb(255 255 255 / .5)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:root{color-scheme:dark}body{margin:0;padding:0;background:#0a0a0a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;overflow:hidden}#root{width:100vw;height:100vh;height:100dvh}.cm-editor{height:100%;font-size:13px}.cm-editor .cm-scroller{font-family:JetBrains Mono,Fira Code,SF Mono,monospace}.cm-editor.cm-focused{outline:none}.placeholder\:text-neutral-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.placeholder\:text-neutral-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-neutral-500:hover{--tw-border-opacity: 1;border-color:rgb(115 115 115 / var(--tw-border-opacity, 1))}.hover\:border-neutral-600:hover{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.hover\:border-neutral-700:hover{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.hover\:border-neutral-800:hover{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.hover\:border-studio-accent\/50:hover{border-color:#3ce6ac80}.hover\:bg-neutral-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800:hover{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800\/30:hover{background-color:#2626264d}.hover\:bg-neutral-800\/50:hover{background-color:#26262680}.hover\:bg-neutral-900:hover{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-800\/60:hover{background-color:#991b1b99}.hover\:bg-red-900\/30:hover{background-color:#7f1d1d4d}.hover\:bg-studio-accent\/25:hover{background-color:#3ce6ac40}.hover\:bg-studio-accent\/80:hover{background-color:#3ce6accc}.hover\:bg-white\/\[0\.06\]:hover{background-color:#ffffff0f}.hover\:text-amber-300:hover{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.hover\:text-green-400:hover{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.hover\:text-neutral-100:hover{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.hover\:text-neutral-200:hover{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.hover\:text-neutral-300:hover{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.hover\:text-neutral-400:hover{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-studio-accent:hover{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:ring-1:hover{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.hover\:ring-white\/30:hover{--tw-ring-color: rgb(255 255 255 / .3)}.hover\:brightness-110:hover{--tw-brightness: brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:border-\[\#3CE6AC\]:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-neutral-600:focus{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent\/40:focus{border-color:#3ce6ac66}.focus\:border-studio-accent\/60:focus{border-color:#3ce6ac99}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-studio-accent\/40:focus{--tw-ring-color: rgb(60 230 172 / .4)}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-studio-accent\/50:focus-visible{--tw-ring-color: rgb(60 230 172 / .5)}.active\:scale-\[0\.97\]:active{--tw-scale-x: .97;--tw-scale-y: .97;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x: .98;--tw-scale-y: .98;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:scale-125{--tw-scale-x: 1.25;--tw-scale-y: 1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media(min-width:640px){.sm\:inline{display:inline}.sm\:flex{display:flex}}@media(min-width:1024px){.lg\:flex{display:flex}}
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>HyperFrames Studio</title>
7
- <script type="module" crossorigin src="/assets/index-Bj3m6A02.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-_h8opaGY.css">
7
+ <script type="module" crossorigin src="/assets/index-18P_dZeo.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BLrgRQSu.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "codemirror": "^6.0.1",
34
34
  "motion": "^12.38.0",
35
- "@hyperframes/core": "0.4.37",
36
- "@hyperframes/player": "0.4.37"
35
+ "@hyperframes/core": "0.4.38",
36
+ "@hyperframes/player": "0.4.38"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.0.0",
@@ -47,7 +47,7 @@
47
47
  "vite": "^6.4.2",
48
48
  "vitest": "^3.2.4",
49
49
  "zustand": "^5.0.0",
50
- "@hyperframes/producer": "0.4.37"
50
+ "@hyperframes/producer": "0.4.38"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "^18.0.0 || ^19.0.0",
package/src/App.tsx CHANGED
@@ -1,11 +1,19 @@
1
- import { useState, useCallback, useRef, useEffect, useMemo, type ReactNode } from "react";
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useRef,
5
+ useEffect,
6
+ useMemo,
7
+ type MouseEvent,
8
+ type ReactNode,
9
+ } from "react";
2
10
  import { useMountEffect } from "./hooks/useMountEffect";
3
11
  import { NLELayout } from "./components/nle/NLELayout";
4
12
  import { SourceEditor } from "./components/editor/SourceEditor";
5
13
  import { LeftSidebar } from "./components/sidebar/LeftSidebar";
6
14
  import { RenderQueue } from "./components/renders/RenderQueue";
7
15
  import { useRenderQueue } from "./components/renders/useRenderQueue";
8
- import { CompositionThumbnail, VideoThumbnail, usePlayerStore } from "./player";
16
+ import { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player";
9
17
  import { AudioWaveform } from "./player/components/AudioWaveform";
10
18
  import type { TimelineElement } from "./player";
11
19
  import { LintModal } from "./components/LintModal";
@@ -40,6 +48,8 @@ import {
40
48
  getTimelineToggleTitle,
41
49
  shouldHandleTimelineToggleHotkey,
42
50
  } from "./utils/timelineDiscovery";
51
+ import { buildFrameCaptureFilename, buildFrameCaptureUrl } from "./utils/frameCapture";
52
+ import { Camera } from "./icons/SystemIcons";
43
53
 
44
54
  interface EditingFile {
45
55
  path: string;
@@ -264,6 +274,7 @@ export function StudioApp() {
264
274
  const [globalDragOver, setGlobalDragOver] = useState(false);
265
275
  const [appToast, setAppToast] = useState<AppToast | null>(null);
266
276
  const [timelineVisible, setTimelineVisible] = useState(true);
277
+ const [captureFrameTime, setCaptureFrameTime] = useState(0);
267
278
  const dragCounterRef = useRef(0);
268
279
  const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
269
280
  const lastBlockedTimelineToastAtRef = useRef(0);
@@ -298,6 +309,26 @@ export function StudioApp() {
298
309
  const toggleTimelineVisibility = useCallback(() => {
299
310
  setTimelineVisible((visible) => !visible);
300
311
  }, []);
312
+ const toggleLeftSidebar = useCallback(() => {
313
+ setLeftCollapsed((collapsed) => !collapsed);
314
+ }, []);
315
+ const refreshCaptureFrameTime = useCallback(() => {
316
+ setCaptureFrameTime(usePlayerStore.getState().currentTime);
317
+ }, []);
318
+
319
+ useMountEffect(() => {
320
+ setCaptureFrameTime(usePlayerStore.getState().currentTime);
321
+ return liveTime.subscribe(setCaptureFrameTime);
322
+ });
323
+
324
+ const captureFrameHref = projectId
325
+ ? buildFrameCaptureUrl({
326
+ projectId,
327
+ compositionPath: activeCompPath,
328
+ currentTime: captureFrameTime,
329
+ })
330
+ : "#";
331
+ const captureFrameFilename = buildFrameCaptureFilename(activeCompPath, captureFrameTime);
301
332
  useMountEffect(() => () => {
302
333
  if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
303
334
  });
@@ -496,6 +527,28 @@ export function StudioApp() {
496
527
  >
497
528
  +
498
529
  </button>
530
+ <button
531
+ type="button"
532
+ onClick={toggleTimelineVisibility}
533
+ className="ml-1 flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-900 hover:text-neutral-200"
534
+ title={getTimelineToggleTitle(true)}
535
+ aria-label="Hide timeline editor"
536
+ >
537
+ <svg
538
+ width="14"
539
+ height="14"
540
+ viewBox="0 0 24 24"
541
+ fill="none"
542
+ stroke="currentColor"
543
+ strokeWidth="1.8"
544
+ strokeLinecap="round"
545
+ strokeLinejoin="round"
546
+ aria-hidden="true"
547
+ >
548
+ <path d="M5 7h14" />
549
+ <path d="m8 11 4 4 4-4" />
550
+ </svg>
551
+ </button>
499
552
  </div>
500
553
  </div>
501
554
  </div>
@@ -787,6 +840,42 @@ export function StudioApp() {
787
840
  toastTimerRef.current = setTimeout(() => setAppToast(null), 4000);
788
841
  }, []);
789
842
 
843
+ const handleCaptureFrameClick = useCallback(
844
+ async (event: MouseEvent<HTMLAnchorElement>) => {
845
+ if (!projectId) return;
846
+ event.preventDefault();
847
+
848
+ const currentTime = usePlayerStore.getState().currentTime;
849
+ setCaptureFrameTime(currentTime);
850
+ const href = buildFrameCaptureUrl({
851
+ projectId,
852
+ compositionPath: activeCompPath,
853
+ currentTime,
854
+ });
855
+ const filename = buildFrameCaptureFilename(activeCompPath, currentTime);
856
+
857
+ try {
858
+ const response = await fetch(href, { cache: "no-store" });
859
+ if (!response.ok) {
860
+ throw new Error(`Capture failed (${response.status})`);
861
+ }
862
+ const blob = await response.blob();
863
+ const blobUrl = URL.createObjectURL(blob);
864
+ const link = document.createElement("a");
865
+ link.href = blobUrl;
866
+ link.download = filename;
867
+ document.body.appendChild(link);
868
+ link.click();
869
+ link.remove();
870
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 0);
871
+ } catch (err) {
872
+ const message = err instanceof Error ? err.message : "Capture failed";
873
+ showToast(message);
874
+ }
875
+ },
876
+ [activeCompPath, projectId, showToast],
877
+ );
878
+
790
879
  const handleTimelineElementDelete = useCallback(
791
880
  async (element: TimelineElement) => {
792
881
  const pid = projectIdRef.current;
@@ -1345,55 +1434,19 @@ export function StudioApp() {
1345
1434
  </div>
1346
1435
  {/* Right: toolbar buttons */}
1347
1436
  <div className="flex items-center gap-1.5">
1348
- <button
1349
- onClick={() => setLeftCollapsed((v) => !v)}
1350
- className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${
1351
- !leftCollapsed
1352
- ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
1353
- : "bg-transparent border-transparent text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800"
1354
- }`}
1355
- title={leftCollapsed ? "Show sidebar" : "Hide sidebar"}
1437
+ <a
1438
+ href={captureFrameHref}
1439
+ download={captureFrameFilename}
1440
+ onClick={handleCaptureFrameClick}
1441
+ onFocus={refreshCaptureFrameTime}
1442
+ onPointerDown={refreshCaptureFrameTime}
1443
+ className="h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border border-neutral-700 text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800"
1444
+ title="Capture current frame"
1445
+ aria-label="Capture current frame"
1356
1446
  >
1357
- <svg
1358
- width="14"
1359
- height="14"
1360
- viewBox="0 0 24 24"
1361
- fill="none"
1362
- stroke="currentColor"
1363
- strokeWidth="1.5"
1364
- strokeLinecap="round"
1365
- strokeLinejoin="round"
1366
- >
1367
- <rect x="3" y="3" width="18" height="18" rx="2" />
1368
- <path d="M9 3v18" />
1369
- </svg>
1370
- </button>
1371
- <button
1372
- type="button"
1373
- onClick={toggleTimelineVisibility}
1374
- className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
1375
- timelineVisible
1376
- ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
1377
- : "text-neutral-300 border-neutral-700 hover:border-neutral-500 hover:bg-neutral-800"
1378
- }`}
1379
- title={getTimelineToggleTitle(timelineVisible)}
1380
- aria-label={timelineVisible ? "Hide timeline editor" : "Show timeline editor"}
1381
- >
1382
- <svg
1383
- width="14"
1384
- height="14"
1385
- viewBox="0 0 24 24"
1386
- fill="none"
1387
- stroke="currentColor"
1388
- strokeWidth="1.5"
1389
- strokeLinecap="round"
1390
- >
1391
- <rect x="3" y="13" width="18" height="8" rx="1" />
1392
- <line x1="3" y1="9" x2="21" y2="9" />
1393
- <line x1="3" y1="5" x2="21" y2="5" />
1394
- </svg>
1395
- <span>Timeline</span>
1396
- </button>
1447
+ <Camera size={14} />
1448
+ <span>Capture</span>
1449
+ </a>
1397
1450
  <button
1398
1451
  onClick={() => setRightCollapsed((v) => !v)}
1399
1452
  className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
@@ -1422,7 +1475,32 @@ export function StudioApp() {
1422
1475
  {/* Main content: sidebar + preview + right panel */}
1423
1476
  <div className="flex flex-1 min-h-0">
1424
1477
  {/* Left sidebar: Compositions + Assets (resizable, collapsible) */}
1425
- {!leftCollapsed && (
1478
+ {leftCollapsed ? (
1479
+ <div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
1480
+ <button
1481
+ type="button"
1482
+ onClick={toggleLeftSidebar}
1483
+ className="flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
1484
+ title="Show sidebar"
1485
+ aria-label="Show sidebar"
1486
+ >
1487
+ <svg
1488
+ width="14"
1489
+ height="14"
1490
+ viewBox="0 0 24 24"
1491
+ fill="none"
1492
+ stroke="currentColor"
1493
+ strokeWidth="1.5"
1494
+ strokeLinecap="round"
1495
+ strokeLinejoin="round"
1496
+ aria-hidden="true"
1497
+ >
1498
+ <path d="M5 4v16" />
1499
+ <path d="m10 7 5 5-5 5" />
1500
+ </svg>
1501
+ </button>
1502
+ </div>
1503
+ ) : (
1426
1504
  <LeftSidebar
1427
1505
  width={leftWidth}
1428
1506
  projectId={projectId}
@@ -1469,6 +1547,7 @@ export function StudioApp() {
1469
1547
  }
1470
1548
  onLint={handleLint}
1471
1549
  linting={linting}
1550
+ onToggleCollapse={toggleLeftSidebar}
1472
1551
  />
1473
1552
  )}
1474
1553
 
@@ -5,6 +5,10 @@ import type { TimelineElement } from "../../player";
5
5
  import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
6
6
  import { NLEPreview } from "./NLEPreview";
7
7
  import { CompositionBreadcrumb, type CompositionLevel } from "./CompositionBreadcrumb";
8
+ import {
9
+ TIMELINE_TOGGLE_SHORTCUT_LABEL,
10
+ getTimelineToggleTitle,
11
+ } from "../../utils/timelineDiscovery";
8
12
 
9
13
  interface NLELayoutProps {
10
14
  projectId: string;
@@ -197,6 +201,7 @@ export const NLELayout = memo(function NLELayout({
197
201
 
198
202
  // Resizable timeline height
199
203
  const [timelineH, setTimelineH] = useState(DEFAULT_TIMELINE_H);
204
+ const isTimelineVisible = timelineVisible ?? true;
200
205
  const isDragging = useRef(false);
201
206
  const containerRef = useRef<HTMLDivElement>(null);
202
207
 
@@ -366,16 +371,11 @@ export const NLELayout = memo(function NLELayout({
366
371
  onNavigate={handleNavigateComposition}
367
372
  />
368
373
  )}
369
- <PlayerControls
370
- onTogglePlay={togglePlay}
371
- onSeek={seek}
372
- timelineVisible={timelineVisible ?? true}
373
- onToggleTimeline={onToggleTimeline}
374
- />
374
+ <PlayerControls onTogglePlay={togglePlay} onSeek={seek} />
375
375
  </div>
376
376
  </div>
377
377
 
378
- {(timelineVisible ?? true) && (
378
+ {isTimelineVisible ? (
379
379
  <>
380
380
  {/* Resize divider */}
381
381
  <div
@@ -417,7 +417,42 @@ export const NLELayout = memo(function NLELayout({
417
417
  {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
418
418
  </div>
419
419
  </>
420
- )}
420
+ ) : onToggleTimeline ? (
421
+ <div className="flex-shrink-0 border-t border-neutral-800/50 bg-neutral-950/96">
422
+ <div className="flex h-10 items-center justify-between px-3">
423
+ <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
424
+ Timeline
425
+ </div>
426
+ <button
427
+ type="button"
428
+ onClick={onToggleTimeline}
429
+ className="flex h-7 items-center gap-1.5 rounded-md border border-neutral-800 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:bg-neutral-900 hover:text-neutral-100"
430
+ title={getTimelineToggleTitle(false)}
431
+ aria-label="Show timeline editor"
432
+ >
433
+ <svg
434
+ width="13"
435
+ height="13"
436
+ viewBox="0 0 24 24"
437
+ fill="none"
438
+ stroke="currentColor"
439
+ strokeWidth="1.7"
440
+ strokeLinecap="round"
441
+ strokeLinejoin="round"
442
+ aria-hidden="true"
443
+ >
444
+ <rect x="3" y="13" width="18" height="8" rx="1" />
445
+ <path d="M7 9h10" />
446
+ <path d="M8 5h8" />
447
+ </svg>
448
+ <span>Show</span>
449
+ <span className="hidden rounded bg-white/5 px-1 py-0.5 font-mono text-[9px] text-neutral-500 sm:inline">
450
+ {TIMELINE_TOGGLE_SHORTCUT_LABEL}
451
+ </span>
452
+ </button>
453
+ </div>
454
+ </div>
455
+ ) : null}
421
456
  </div>
422
457
  );
423
458
  });
@@ -35,6 +35,7 @@ interface LeftSidebarProps {
35
35
  codeChildren?: ReactNode;
36
36
  onLint?: () => void;
37
37
  linting?: boolean;
38
+ onToggleCollapse?: () => void;
38
39
  }
39
40
 
40
41
  export const LeftSidebar = memo(function LeftSidebar({
@@ -57,6 +58,7 @@ export const LeftSidebar = memo(function LeftSidebar({
57
58
  codeChildren,
58
59
  onLint,
59
60
  linting,
61
+ onToggleCollapse,
60
62
  }: LeftSidebarProps) {
61
63
  const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
62
64
 
@@ -122,6 +124,30 @@ export const LeftSidebar = memo(function LeftSidebar({
122
124
  >
123
125
  Assets
124
126
  </button>
127
+ {onToggleCollapse && (
128
+ <button
129
+ type="button"
130
+ onClick={onToggleCollapse}
131
+ className="mx-1 my-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
132
+ title="Hide sidebar"
133
+ aria-label="Hide sidebar"
134
+ >
135
+ <svg
136
+ width="14"
137
+ height="14"
138
+ viewBox="0 0 24 24"
139
+ fill="none"
140
+ stroke="currentColor"
141
+ strokeWidth="1.5"
142
+ strokeLinecap="round"
143
+ strokeLinejoin="round"
144
+ aria-hidden="true"
145
+ >
146
+ <path d="m14 7-5 5 5 5" />
147
+ <path d="M19 4v16" />
148
+ </svg>
149
+ </button>
150
+ )}
125
151
  </div>
126
152
 
127
153
  {/* Tab content */}
@@ -53,6 +53,7 @@ import {
53
53
  CaretRight,
54
54
  ClipboardText,
55
55
  ArrowCounterClockwise,
56
+ Camera as PhCamera,
56
57
  Gear,
57
58
  } from "@phosphor-icons/react";
58
59
  import type { Icon as PhosphorIcon, IconProps as PhosphorIconProps } from "@phosphor-icons/react";
@@ -127,4 +128,5 @@ export const ChevronDown = makeIcon(CaretDown);
127
128
  export const ChevronRight = makeIcon(CaretRight);
128
129
  export const ClipboardList = makeIcon(ClipboardText);
129
130
  export const RotateCcw = makeIcon(ArrowCounterClockwise);
131
+ export const Camera = makeIcon(PhCamera);
130
132
  export const Settings = makeIcon(Gear);
@@ -1,10 +1,6 @@
1
1
  import { useRef, useState, useCallback, useEffect, memo } from "react";
2
2
  import { useMountEffect } from "../../hooks/useMountEffect";
3
- import {
4
- TIMELINE_TOGGLE_SHORTCUT_LABEL,
5
- getTimelineToggleTitle,
6
- } from "../../utils/timelineDiscovery";
7
- import { formatFrameTime, frameToSeconds, formatTime } from "../lib/time";
3
+ import { formatFrameTime, frameToSeconds, stepFrameTime, formatTime } from "../lib/time";
8
4
  import { usePlayerStore, liveTime } from "../store/playerStore";
9
5
 
10
6
  const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
@@ -30,15 +26,11 @@ export function resolveSeekPercent(clientX: number, rectLeft: number, rectWidth:
30
26
  interface PlayerControlsProps {
31
27
  onTogglePlay: () => void;
32
28
  onSeek: (time: number) => void;
33
- timelineVisible?: boolean;
34
- onToggleTimeline?: () => void;
35
29
  }
36
30
 
37
31
  export const PlayerControls = memo(function PlayerControls({
38
32
  onTogglePlay,
39
33
  onSeek,
40
- timelineVisible,
41
- onToggleTimeline,
42
34
  }: PlayerControlsProps) {
43
35
  // Subscribe to only the fields we render — each selector prevents cascading re-renders
44
36
  const isPlaying = usePlayerStore((s) => s.isPlaying);
@@ -216,10 +208,10 @@ export const PlayerControls = memo(function PlayerControls({
216
208
  const step = e.shiftKey ? 10 : 1;
217
209
  if (e.key === "ArrowLeft") {
218
210
  e.preventDefault();
219
- onSeek(Math.max(0, currentTimeRef.current - frameToSeconds(step)));
211
+ onSeek(stepFrameTime(currentTimeRef.current, -step));
220
212
  } else if (e.key === "ArrowRight") {
221
213
  e.preventDefault();
222
- onSeek(Math.min(duration, currentTimeRef.current + frameToSeconds(step)));
214
+ onSeek(Math.min(duration, stepFrameTime(currentTimeRef.current, step)));
223
215
  }
224
216
  },
225
217
  [timelineReady, duration, onSeek],
@@ -437,39 +429,6 @@ export const PlayerControls = memo(function PlayerControls({
437
429
  </span>
438
430
  ))}
439
431
  </div>
440
-
441
- {/* Timeline toggle */}
442
- {onToggleTimeline !== undefined && (
443
- <button
444
- type="button"
445
- onClick={onToggleTimeline}
446
- className={`h-7 flex items-center gap-1.5 rounded-md border px-2.5 text-[11px] font-medium transition-colors ${
447
- timelineVisible
448
- ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
449
- : "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800"
450
- }`}
451
- title={getTimelineToggleTitle(Boolean(timelineVisible))}
452
- aria-label={timelineVisible ? "Hide timeline editor" : "Show timeline editor"}
453
- >
454
- <svg
455
- width="13"
456
- height="13"
457
- viewBox="0 0 24 24"
458
- fill="none"
459
- stroke="currentColor"
460
- strokeWidth="2"
461
- strokeLinecap="round"
462
- >
463
- <rect x="3" y="13" width="18" height="8" rx="1" />
464
- <line x1="3" y1="9" x2="21" y2="9" />
465
- <line x1="3" y1="5" x2="21" y2="5" />
466
- </svg>
467
- <span>Timeline</span>
468
- <span className="hidden md:inline rounded bg-black/20 px-1 py-0.5 text-[9px] font-mono opacity-70">
469
- {TIMELINE_TOGGLE_SHORTCUT_LABEL}
470
- </span>
471
- </button>
472
- )}
473
432
  </div>
474
433
  );
475
434
  });
@@ -63,12 +63,12 @@ const TRACK_STYLES: Record<string, TimelineTrackStyle> = {
63
63
  const DEFAULT_TRACK_STYLE: TimelineTrackStyle = createTrackStyle();
64
64
 
65
65
  export const defaultTimelineTheme: TimelineTheme = {
66
- shellBackground: "#0A0E15",
66
+ shellBackground: "#0A0A0B",
67
67
  shellBorder: "rgba(255,255,255,0.05)",
68
68
  rulerBorder: "rgba(255,255,255,0.045)",
69
- rowBackground: "#0A0E15",
69
+ rowBackground: "#0A0A0B",
70
70
  rowBorder: "rgba(255,255,255,0.05)",
71
- gutterBackground: "#0D121B",
71
+ gutterBackground: "#0A0A0B",
72
72
  gutterBorder: "rgba(255,255,255,0.05)",
73
73
  textPrimary: "#E8EDF5",
74
74
  textSecondary: "#8391A8",
@@ -1,7 +1,7 @@
1
1
  import { useRef, useCallback } from "react";
2
2
  import { usePlayerStore, liveTime, type TimelineElement } from "../store/playerStore";
3
3
  import { useMountEffect } from "../../hooks/useMountEffect";
4
- import { frameToSeconds, STUDIO_PREVIEW_FPS } from "../lib/time";
4
+ import { stepFrameTime, STUDIO_PREVIEW_FPS } from "../lib/time";
5
5
  import { useCaptionStore } from "../../captions/store";
6
6
 
7
7
  interface PlaybackAdapter {
@@ -697,7 +697,7 @@ export function useTimelinePlayer() {
697
697
  (deltaFrames: number) => {
698
698
  const adapter = getAdapter();
699
699
  const currentTime = adapter?.getTime() ?? usePlayerStore.getState().currentTime;
700
- seek(currentTime + frameToSeconds(deltaFrames, STUDIO_PREVIEW_FPS));
700
+ seek(stepFrameTime(currentTime, deltaFrames, STUDIO_PREVIEW_FPS));
701
701
  },
702
702
  [getAdapter, seek],
703
703
  );
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { formatFrameTime, frameToSeconds, secondsToFrame, formatTime } from "./time";
2
+ import { formatFrameTime, frameToSeconds, secondsToFrame, stepFrameTime, formatTime } from "./time";
3
3
 
4
4
  describe("formatTime", () => {
5
5
  it("formats zero seconds", () => {
@@ -72,4 +72,14 @@ describe("frame helpers", () => {
72
72
  it("formats current and total frame display", () => {
73
73
  expect(formatFrameTime(1, 5)).toBe("30f / 150f");
74
74
  });
75
+
76
+ it("steps from a truncated runtime time by integer frame index", () => {
77
+ expect(stepFrameTime(0.0333333, 1)).toBe(2 / 30);
78
+ expect(stepFrameTime(0.0666666, 1)).toBe(3 / 30);
79
+ expect(stepFrameTime(0.0666666, -1)).toBe(1 / 30);
80
+ });
81
+
82
+ it("clamps frame stepping at zero", () => {
83
+ expect(stepFrameTime(0, -1)).toBe(0);
84
+ });
75
85
  });
@@ -19,6 +19,12 @@ export function frameToSeconds(frame: number, fps = STUDIO_PREVIEW_FPS): number
19
19
  return frame / fps;
20
20
  }
21
21
 
22
+ export function stepFrameTime(time: number, deltaFrames: number, fps = STUDIO_PREVIEW_FPS): number {
23
+ const currentFrame = secondsToFrame(time, fps);
24
+ const nextFrame = Math.max(0, currentFrame + deltaFrames);
25
+ return frameToSeconds(nextFrame, fps);
26
+ }
27
+
22
28
  export function formatFrameTime(time: number, duration: number, fps = STUDIO_PREVIEW_FPS): string {
23
29
  const currentFrame = secondsToFrame(time, fps);
24
30
  const totalFrames = secondsToFrame(duration, fps);