@hyperframes/studio 0.4.16 → 0.4.18

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-5{bottom:1.25rem}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-5{left:1.25rem}.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-\[140\]{z-index:140}.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\.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-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-\[160px\]{width:160px}.w-\[320px\]{width:320px}.w-\[56px\]{width:56px}.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-\[72px\]{min-width:72px}.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}.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}.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\/20{background-color:#0003}.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\/\[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\.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}.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-70{opacity:.7}.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)}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-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-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\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.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\: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:768px){.md\:inline{display:inline}}
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-RzXlAX2g.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CVm-zeM9.css">
7
+ <script type="module" crossorigin src="/assets/index-D0VntLIQ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-kT65pCwW.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.16",
3
+ "version": "0.4.18",
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.16",
36
- "@hyperframes/player": "0.4.16"
35
+ "@hyperframes/core": "0.4.18",
36
+ "@hyperframes/player": "0.4.18"
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.16"
50
+ "@hyperframes/producer": "0.4.18"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "^18.0.0 || ^19.0.0",
package/src/App.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useState, useCallback, useRef, useEffect, useMemo, type ReactNode } from "react";
2
2
  import { useMountEffect } from "./hooks/useMountEffect";
3
3
  import { NLELayout } from "./components/nle/NLELayout";
4
+ import { TimelineEditorNotice } from "./components/nle/TimelineEditorNotice";
4
5
  import { SourceEditor } from "./components/editor/SourceEditor";
5
6
  import { LeftSidebar } from "./components/sidebar/LeftSidebar";
6
7
  import { RenderQueue } from "./components/renders/RenderQueue";
@@ -12,6 +13,15 @@ import { LintModal } from "./components/LintModal";
12
13
  import type { LintFinding } from "./components/LintModal";
13
14
  import { MediaPreview } from "./components/MediaPreview";
14
15
  import { isMediaFile } from "./utils/mediaTypes";
16
+ import {
17
+ buildTimelineAssetId,
18
+ buildTimelineAssetInsertHtml,
19
+ buildTimelineFileDropPlacements,
20
+ getTimelineAssetKind,
21
+ insertTimelineAssetIntoSource,
22
+ resolveTimelineAssetSrc,
23
+ type TimelineAssetKind,
24
+ } from "./utils/timelineAssetDrop";
15
25
  import { CaptionOverlay } from "./captions/components/CaptionOverlay";
16
26
  import { CaptionPropertyPanel } from "./captions/components/CaptionPropertyPanel";
17
27
  import { CaptionTimeline } from "./captions/components/CaptionTimeline";
@@ -28,7 +38,6 @@ import {
28
38
  getTimelineZoomPercent,
29
39
  } from "./player/components/timelineZoom";
30
40
  import {
31
- TIMELINE_TOGGLE_SHORTCUT_LABEL,
32
41
  getTimelineEditorHintDismissed,
33
42
  getTimelineToggleTitle,
34
43
  setTimelineEditorHintDismissed,
@@ -40,6 +49,61 @@ interface EditingFile {
40
49
  content: string | null;
41
50
  }
42
51
 
52
+ interface AppToast {
53
+ message: string;
54
+ tone: "error" | "info";
55
+ }
56
+
57
+ const DEFAULT_TIMELINE_ASSET_DURATION: Record<TimelineAssetKind, number> = {
58
+ image: 3,
59
+ video: 5,
60
+ audio: 5,
61
+ };
62
+
63
+ function collectHtmlIds(source: string): string[] {
64
+ return Array.from(source.matchAll(/\bid="([^"]+)"/g), (match) => match[1] ?? "");
65
+ }
66
+
67
+ async function resolveDroppedAssetDuration(
68
+ projectId: string,
69
+ assetPath: string,
70
+ kind: TimelineAssetKind,
71
+ ): Promise<number> {
72
+ if (kind === "image") return DEFAULT_TIMELINE_ASSET_DURATION.image;
73
+
74
+ const media = document.createElement(kind === "video" ? "video" : "audio");
75
+ media.preload = "metadata";
76
+ media.src = `/api/projects/${projectId}/preview/${assetPath}`;
77
+
78
+ const duration = await new Promise<number>((resolve) => {
79
+ const timeout = window.setTimeout(() => resolve(DEFAULT_TIMELINE_ASSET_DURATION[kind]), 3000);
80
+ const finalize = (value: number) => {
81
+ window.clearTimeout(timeout);
82
+ resolve(value);
83
+ };
84
+
85
+ media.addEventListener(
86
+ "loadedmetadata",
87
+ () => {
88
+ const raw = Number(media.duration);
89
+ finalize(
90
+ Number.isFinite(raw) && raw > 0
91
+ ? Math.round(raw * 100) / 100
92
+ : DEFAULT_TIMELINE_ASSET_DURATION[kind],
93
+ );
94
+ },
95
+ { once: true },
96
+ );
97
+ media.addEventListener("error", () => finalize(DEFAULT_TIMELINE_ASSET_DURATION[kind]), {
98
+ once: true,
99
+ });
100
+ });
101
+
102
+ media.src = "";
103
+ media.load();
104
+ return duration;
105
+ }
106
+
43
107
  // ── Main App ──
44
108
 
45
109
  export function StudioApp() {
@@ -201,12 +265,14 @@ export function StudioApp() {
201
265
  }
202
266
  }, [captionHasSelection, captionEditMode]);
203
267
  const [globalDragOver, setGlobalDragOver] = useState(false);
204
- const [uploadToast, setUploadToast] = useState<string | null>(null);
268
+ const [appToast, setAppToast] = useState<AppToast | null>(null);
205
269
  const [timelineVisible, setTimelineVisible] = useState(true);
206
270
  const [timelineEditorHintDismissed, setTimelineEditorHintState] = useState(
207
271
  getTimelineEditorHintDismissed,
208
272
  );
209
273
  const dragCounterRef = useRef(0);
274
+ const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
275
+ const lastBlockedTimelineToastAtRef = useRef(0);
210
276
  const previewHotkeyWindowRef = useRef<Window | null>(null);
211
277
  const panelDragRef = useRef<{
212
278
  side: "left" | "right";
@@ -238,6 +304,9 @@ export function StudioApp() {
238
304
  const toggleTimelineVisibility = useCallback(() => {
239
305
  setTimelineVisible((visible) => !visible);
240
306
  }, []);
307
+ useMountEffect(() => () => {
308
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
309
+ });
241
310
  const dismissTimelineEditorHint = useCallback(() => {
242
311
  setTimelineEditorHintState(true);
243
312
  setTimelineEditorHintDismissed(true);
@@ -380,31 +449,6 @@ export function StudioApp() {
380
449
  );
381
450
  const timelineToolbar = (
382
451
  <div className="border-b border-neutral-800/40 bg-neutral-950/96">
383
- {timelineVisible && timelineElements.length > 0 && !timelineEditorHintDismissed && (
384
- <div className="px-3 pt-3">
385
- <div className="flex items-start justify-between gap-3 rounded-xl border border-studio-accent/20 bg-studio-accent/[0.07] px-3 py-3">
386
- <div className="min-w-0">
387
- <div className="text-[11px] font-semibold text-neutral-100">Timeline editor</div>
388
- <p className="mt-1 text-[11px] leading-5 text-neutral-300">
389
- Drag clips to move timing, and drag clip edges to resize them when handles are
390
- available. Hide the panel anytime and bring it back with{" "}
391
- <span className="font-mono text-[10px] text-studio-accent">
392
- {TIMELINE_TOGGLE_SHORTCUT_LABEL}
393
- </span>
394
- .
395
- </p>
396
- </div>
397
- <button
398
- type="button"
399
- onClick={dismissTimelineEditorHint}
400
- className="flex-shrink-0 rounded-md border border-neutral-700 px-2 py-1 text-[10px] font-medium text-neutral-300 transition-colors hover:border-neutral-500 hover:text-neutral-100"
401
- >
402
- Dismiss
403
- </button>
404
- </div>
405
- </div>
406
- )}
407
-
408
452
  <div className="flex items-center justify-between px-3 py-2">
409
453
  <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
410
454
  Timeline
@@ -732,7 +776,135 @@ export function StudioApp() {
732
776
  [activeCompPath],
733
777
  );
734
778
 
735
- // ── File Management Handlers ──
779
+ const showToast = useCallback((message: string, tone: AppToast["tone"] = "error") => {
780
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
781
+ setAppToast({ message, tone });
782
+ toastTimerRef.current = setTimeout(() => setAppToast(null), 4000);
783
+ }, []);
784
+
785
+ const handleTimelineElementDelete = useCallback(
786
+ async (element: TimelineElement) => {
787
+ const pid = projectIdRef.current;
788
+ if (!pid) throw new Error("No active project");
789
+
790
+ const targetPath = element.sourceFile || activeCompPath || "index.html";
791
+ try {
792
+ const response = await fetch(
793
+ `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
794
+ );
795
+ if (!response.ok) {
796
+ throw new Error(`Failed to read ${targetPath}`);
797
+ }
798
+
799
+ const data = (await response.json()) as { content?: string };
800
+ const originalContent = data.content;
801
+ if (typeof originalContent !== "string") {
802
+ throw new Error(`Missing file contents for ${targetPath}`);
803
+ }
804
+
805
+ const patchTarget = element.domId
806
+ ? { id: element.domId, selector: element.selector, selectorIndex: element.selectorIndex }
807
+ : element.selector
808
+ ? { selector: element.selector, selectorIndex: element.selectorIndex }
809
+ : null;
810
+ if (!patchTarget) {
811
+ throw new Error(`Timeline element ${element.id} is missing a patchable target`);
812
+ }
813
+
814
+ const resolvedTargetPath = targetPath || "index.html";
815
+ const remainingElements = timelineElements.filter(
816
+ (timelineElement) =>
817
+ (timelineElement.key ?? timelineElement.id) !== (element.key ?? element.id) &&
818
+ (timelineElement.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
819
+ );
820
+ const trackZIndices = buildTrackZIndexMap(
821
+ remainingElements.map((timelineElement) => timelineElement.track),
822
+ );
823
+
824
+ const removeResponse = await fetch(
825
+ `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
826
+ {
827
+ method: "POST",
828
+ headers: { "Content-Type": "application/json" },
829
+ body: JSON.stringify({ target: patchTarget }),
830
+ },
831
+ );
832
+ if (!removeResponse.ok) {
833
+ throw new Error(`Failed to delete ${element.id} from ${targetPath}`);
834
+ }
835
+
836
+ const removeData = (await removeResponse.json()) as {
837
+ changed?: boolean;
838
+ content?: string;
839
+ };
840
+ let patchedContent =
841
+ typeof removeData.content === "string" ? removeData.content : originalContent;
842
+ for (const timelineElement of remainingElements) {
843
+ const elementTarget = timelineElement.domId
844
+ ? {
845
+ id: timelineElement.domId,
846
+ selector: timelineElement.selector,
847
+ selectorIndex: timelineElement.selectorIndex,
848
+ }
849
+ : timelineElement.selector
850
+ ? {
851
+ selector: timelineElement.selector,
852
+ selectorIndex: timelineElement.selectorIndex,
853
+ }
854
+ : null;
855
+ if (!elementTarget) continue;
856
+ const nextZIndex = trackZIndices.get(timelineElement.track);
857
+ if (nextZIndex == null) continue;
858
+ patchedContent = applyPatchByTarget(patchedContent, elementTarget, {
859
+ type: "inline-style",
860
+ property: "z-index",
861
+ value: String(nextZIndex),
862
+ });
863
+ }
864
+
865
+ const saveResponse = await fetch(
866
+ `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
867
+ {
868
+ method: "PUT",
869
+ headers: { "Content-Type": "text/plain" },
870
+ body: patchedContent,
871
+ },
872
+ );
873
+ if (!saveResponse.ok) {
874
+ throw new Error(`Failed to save ${targetPath}`);
875
+ }
876
+
877
+ if (editingPathRef.current === targetPath) {
878
+ setEditingFile({ path: targetPath, content: patchedContent });
879
+ }
880
+
881
+ usePlayerStore
882
+ .getState()
883
+ .setElements(
884
+ timelineElements.filter(
885
+ (timelineElement) =>
886
+ (timelineElement.key ?? timelineElement.id) !== (element.key ?? element.id),
887
+ ),
888
+ );
889
+ usePlayerStore.getState().setSelectedElementId(null);
890
+ setRefreshKey((k) => k + 1);
891
+ } catch (error) {
892
+ const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
893
+ showToast(message);
894
+ }
895
+ },
896
+ [activeCompPath, showToast, timelineElements],
897
+ );
898
+
899
+ const handleBlockedTimelineEdit = useCallback(
900
+ (_element: TimelineElement) => {
901
+ const now = Date.now();
902
+ if (now - lastBlockedTimelineToastAtRef.current < 1500) return;
903
+ lastBlockedTimelineToastAtRef.current = now;
904
+ showToast("This clip can’t be moved or resized from the timeline yet.", "info");
905
+ },
906
+ [showToast],
907
+ );
736
908
 
737
909
  const refreshFileTree = useCallback(async () => {
738
910
  const pid = projectIdRef.current;
@@ -742,6 +914,171 @@ export function StudioApp() {
742
914
  if (data.files) setFileTree(data.files);
743
915
  }, []);
744
916
 
917
+ const uploadProjectFiles = useCallback(
918
+ async (files: Iterable<File>, dir?: string): Promise<string[]> => {
919
+ const pid = projectIdRef.current;
920
+ const fileList = Array.from(files);
921
+ if (!pid || fileList.length === 0) return [];
922
+
923
+ const formData = new FormData();
924
+ for (const file of fileList) {
925
+ formData.append("file", file);
926
+ }
927
+
928
+ const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
929
+ try {
930
+ const res = await fetch(`/api/projects/${pid}/upload${qs}`, {
931
+ method: "POST",
932
+ body: formData,
933
+ });
934
+ if (res.ok) {
935
+ const data = await res.json();
936
+ if (data.skipped?.length) {
937
+ showToast(`Skipped (too large): ${data.skipped.join(", ")}`);
938
+ }
939
+ if (data.invalid?.length) {
940
+ const names = data.invalid.map((entry: { name: string }) => entry.name).join(", ");
941
+ showToast(`Unsupported media skipped: ${names}`);
942
+ }
943
+ await refreshFileTree();
944
+ setRefreshKey((k) => k + 1);
945
+ return Array.isArray(data.files) ? data.files : [];
946
+ } else if (res.status === 413) {
947
+ showToast("Upload rejected: payload too large");
948
+ } else {
949
+ showToast(`Upload failed (${res.status})`);
950
+ }
951
+ } catch {
952
+ showToast("Upload failed: network error");
953
+ }
954
+ return [];
955
+ },
956
+ [refreshFileTree, showToast],
957
+ );
958
+
959
+ const handleTimelineAssetDrop = useCallback(
960
+ async (assetPath: string, placement: Pick<TimelineElement, "start" | "track">) => {
961
+ const pid = projectIdRef.current;
962
+ if (!pid) throw new Error("No active project");
963
+
964
+ const kind = getTimelineAssetKind(assetPath);
965
+ if (!kind) {
966
+ showToast("Only image, video, and audio assets can be dropped onto the timeline.");
967
+ return;
968
+ }
969
+
970
+ const targetPath = activeCompPath || "index.html";
971
+ try {
972
+ const response = await fetch(
973
+ `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
974
+ );
975
+ if (!response.ok) {
976
+ throw new Error(`Failed to read ${targetPath}`);
977
+ }
978
+
979
+ const data = (await response.json()) as { content?: string };
980
+ const originalContent = data.content;
981
+ if (typeof originalContent !== "string") {
982
+ throw new Error(`Missing file contents for ${targetPath}`);
983
+ }
984
+
985
+ const normalizedStart = Number(formatTimelineAttributeNumber(placement.start));
986
+ const normalizedDuration = Number(
987
+ formatTimelineAttributeNumber(await resolveDroppedAssetDuration(pid, assetPath, kind)),
988
+ );
989
+ const newId = buildTimelineAssetId(assetPath, collectHtmlIds(originalContent));
990
+ const resolvedAssetSrc = resolveTimelineAssetSrc(targetPath, assetPath);
991
+
992
+ const resolvedTargetPath = targetPath || "index.html";
993
+ const relevantElements = timelineElements.filter(
994
+ (timelineElement) =>
995
+ (timelineElement.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
996
+ );
997
+ const trackZIndices = buildTrackZIndexMap([
998
+ ...relevantElements.map((timelineElement) => timelineElement.track),
999
+ placement.track,
1000
+ ]);
1001
+
1002
+ let patchedContent = originalContent;
1003
+ for (const timelineElement of relevantElements) {
1004
+ const elementTarget = timelineElement.domId
1005
+ ? {
1006
+ id: timelineElement.domId,
1007
+ selector: timelineElement.selector,
1008
+ selectorIndex: timelineElement.selectorIndex,
1009
+ }
1010
+ : timelineElement.selector
1011
+ ? {
1012
+ selector: timelineElement.selector,
1013
+ selectorIndex: timelineElement.selectorIndex,
1014
+ }
1015
+ : null;
1016
+ if (!elementTarget) continue;
1017
+ const nextZIndex = trackZIndices.get(timelineElement.track);
1018
+ if (nextZIndex == null) continue;
1019
+ patchedContent = applyPatchByTarget(patchedContent, elementTarget, {
1020
+ type: "inline-style",
1021
+ property: "z-index",
1022
+ value: String(nextZIndex),
1023
+ });
1024
+ }
1025
+
1026
+ patchedContent = insertTimelineAssetIntoSource(
1027
+ patchedContent,
1028
+ buildTimelineAssetInsertHtml({
1029
+ id: newId,
1030
+ assetPath: resolvedAssetSrc,
1031
+ kind,
1032
+ start: normalizedStart,
1033
+ duration: normalizedDuration,
1034
+ track: placement.track,
1035
+ zIndex: trackZIndices.get(placement.track) ?? 1,
1036
+ }),
1037
+ );
1038
+
1039
+ const saveResponse = await fetch(
1040
+ `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
1041
+ {
1042
+ method: "PUT",
1043
+ headers: { "Content-Type": "text/plain" },
1044
+ body: patchedContent,
1045
+ },
1046
+ );
1047
+ if (!saveResponse.ok) {
1048
+ throw new Error(`Failed to save ${targetPath}`);
1049
+ }
1050
+
1051
+ if (editingPathRef.current === targetPath) {
1052
+ setEditingFile({ path: targetPath, content: patchedContent });
1053
+ }
1054
+
1055
+ setRefreshKey((k) => k + 1);
1056
+ } catch (error) {
1057
+ const message =
1058
+ error instanceof Error ? error.message : "Failed to drop asset onto timeline";
1059
+ showToast(message);
1060
+ }
1061
+ },
1062
+ [activeCompPath, showToast, timelineElements],
1063
+ );
1064
+
1065
+ const handleTimelineFileDrop = useCallback(
1066
+ async (files: File[], placement?: Pick<TimelineElement, "start" | "track">) => {
1067
+ const uploaded = await uploadProjectFiles(files);
1068
+ if (uploaded.length === 0) return;
1069
+ const placements = buildTimelineFileDropPlacements(
1070
+ placement ?? { start: 0, track: 0 },
1071
+ uploaded.length,
1072
+ );
1073
+ for (const [index, assetPath] of uploaded.entries()) {
1074
+ await handleTimelineAssetDrop(assetPath, placements[index] ?? placements[0]);
1075
+ }
1076
+ },
1077
+ [handleTimelineAssetDrop, uploadProjectFiles],
1078
+ );
1079
+
1080
+ // ── File Management Handlers ──
1081
+
745
1082
  const handleCreateFile = useCallback(
746
1083
  async (path: string) => {
747
1084
  const pid = projectIdRef.current;
@@ -855,44 +1192,11 @@ export function StudioApp() {
855
1192
 
856
1193
  const handleMoveFile = handleRenameFile;
857
1194
 
858
- const showUploadToast = useCallback((msg: string) => {
859
- setUploadToast(msg);
860
- setTimeout(() => setUploadToast(null), 4000);
861
- }, []);
862
-
863
1195
  const handleImportFiles = useCallback(
864
- async (files: FileList, dir?: string) => {
865
- const pid = projectIdRef.current;
866
- if (!pid || files.length === 0) return;
867
-
868
- const formData = new FormData();
869
- for (const file of Array.from(files)) {
870
- formData.append("file", file);
871
- }
872
-
873
- const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
874
- try {
875
- const res = await fetch(`/api/projects/${pid}/upload${qs}`, {
876
- method: "POST",
877
- body: formData,
878
- });
879
- if (res.ok) {
880
- const data = await res.json();
881
- if (data.skipped?.length) {
882
- showUploadToast(`Skipped (too large): ${data.skipped.join(", ")}`);
883
- }
884
- await refreshFileTree();
885
- setRefreshKey((k) => k + 1);
886
- } else if (res.status === 413) {
887
- showUploadToast("Upload rejected: payload too large");
888
- } else {
889
- showUploadToast(`Upload failed (${res.status})`);
890
- }
891
- } catch {
892
- showUploadToast("Upload failed: network error");
893
- }
1196
+ async (files: FileList | File[], dir?: string) => {
1197
+ void uploadProjectFiles(Array.from(files), dir);
894
1198
  },
895
- [refreshFileTree, showUploadToast],
1199
+ [uploadProjectFiles],
896
1200
  );
897
1201
 
898
1202
  const handleLint = useCallback(async () => {
@@ -1155,8 +1459,12 @@ export function StudioApp() {
1155
1459
  activeCompositionPath={activeCompPath}
1156
1460
  timelineToolbar={timelineToolbar}
1157
1461
  renderClipContent={renderClipContent}
1462
+ onDeleteElement={handleTimelineElementDelete}
1463
+ onAssetDrop={handleTimelineAssetDrop}
1464
+ onFileDrop={handleTimelineFileDrop}
1158
1465
  onMoveElement={handleTimelineElementMove}
1159
1466
  onResizeElement={handleTimelineElementResize}
1467
+ onBlockedEditAttempt={handleBlockedTimelineEdit}
1160
1468
  onCompIdToSrcChange={setCompIdToSrc}
1161
1469
  onCompositionChange={(compPath) => {
1162
1470
  // Sync activeCompPath when user drills down via timeline double-click
@@ -1267,6 +1575,12 @@ export function StudioApp() {
1267
1575
  )}
1268
1576
  </div>
1269
1577
 
1578
+ {timelineElements.length > 0 && !timelineEditorHintDismissed && (
1579
+ <div className="pointer-events-none absolute bottom-5 left-5 z-[140]">
1580
+ <TimelineEditorNotice onDismiss={dismissTimelineEditorHint} />
1581
+ </div>
1582
+ )}
1583
+
1270
1584
  {/* Lint modal */}
1271
1585
  {lintModal !== null && projectId && (
1272
1586
  <LintModal findings={lintModal} projectId={projectId} onClose={() => setLintModal(null)} />
@@ -1306,9 +1620,15 @@ export function StudioApp() {
1306
1620
  </div>
1307
1621
  </div>
1308
1622
  )}
1309
- {uploadToast && (
1310
- <div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-[91] px-4 py-2 rounded-lg bg-red-900/90 border border-red-700/50 text-sm text-red-200 shadow-lg animate-in fade-in slide-in-from-bottom-2">
1311
- {uploadToast}
1623
+ {appToast && (
1624
+ <div
1625
+ className={`absolute bottom-6 left-1/2 -translate-x-1/2 z-[91] px-4 py-2 rounded-lg border text-sm shadow-lg animate-in fade-in slide-in-from-bottom-2 ${
1626
+ appToast.tone === "error"
1627
+ ? "bg-red-900/90 border-red-700/50 text-red-200"
1628
+ : "bg-neutral-900/95 border-neutral-700/60 text-neutral-100"
1629
+ }`}
1630
+ >
1631
+ {appToast.message}
1312
1632
  </div>
1313
1633
  )}
1314
1634
  </div>