@hyperframes/studio 0.4.15 → 0.4.17

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-JZr8f8y8.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.15",
3
+ "version": "0.4.17",
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.15",
36
- "@hyperframes/player": "0.4.15"
35
+ "@hyperframes/core": "0.4.17",
36
+ "@hyperframes/player": "0.4.17"
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.15"
50
+ "@hyperframes/producer": "0.4.17"
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";
@@ -28,7 +29,6 @@ import {
28
29
  getTimelineZoomPercent,
29
30
  } from "./player/components/timelineZoom";
30
31
  import {
31
- TIMELINE_TOGGLE_SHORTCUT_LABEL,
32
32
  getTimelineEditorHintDismissed,
33
33
  getTimelineToggleTitle,
34
34
  setTimelineEditorHintDismissed,
@@ -40,6 +40,11 @@ interface EditingFile {
40
40
  content: string | null;
41
41
  }
42
42
 
43
+ interface AppToast {
44
+ message: string;
45
+ tone: "error" | "info";
46
+ }
47
+
43
48
  // ── Main App ──
44
49
 
45
50
  export function StudioApp() {
@@ -201,12 +206,14 @@ export function StudioApp() {
201
206
  }
202
207
  }, [captionHasSelection, captionEditMode]);
203
208
  const [globalDragOver, setGlobalDragOver] = useState(false);
204
- const [uploadToast, setUploadToast] = useState<string | null>(null);
209
+ const [appToast, setAppToast] = useState<AppToast | null>(null);
205
210
  const [timelineVisible, setTimelineVisible] = useState(true);
206
211
  const [timelineEditorHintDismissed, setTimelineEditorHintState] = useState(
207
212
  getTimelineEditorHintDismissed,
208
213
  );
209
214
  const dragCounterRef = useRef(0);
215
+ const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
216
+ const lastBlockedTimelineToastAtRef = useRef(0);
210
217
  const previewHotkeyWindowRef = useRef<Window | null>(null);
211
218
  const panelDragRef = useRef<{
212
219
  side: "left" | "right";
@@ -238,6 +245,9 @@ export function StudioApp() {
238
245
  const toggleTimelineVisibility = useCallback(() => {
239
246
  setTimelineVisible((visible) => !visible);
240
247
  }, []);
248
+ useMountEffect(() => () => {
249
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
250
+ });
241
251
  const dismissTimelineEditorHint = useCallback(() => {
242
252
  setTimelineEditorHintState(true);
243
253
  setTimelineEditorHintDismissed(true);
@@ -380,31 +390,6 @@ export function StudioApp() {
380
390
  );
381
391
  const timelineToolbar = (
382
392
  <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
393
  <div className="flex items-center justify-between px-3 py-2">
409
394
  <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
410
395
  Timeline
@@ -855,11 +840,22 @@ export function StudioApp() {
855
840
 
856
841
  const handleMoveFile = handleRenameFile;
857
842
 
858
- const showUploadToast = useCallback((msg: string) => {
859
- setUploadToast(msg);
860
- setTimeout(() => setUploadToast(null), 4000);
843
+ const showToast = useCallback((message: string, tone: AppToast["tone"] = "error") => {
844
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
845
+ setAppToast({ message, tone });
846
+ toastTimerRef.current = setTimeout(() => setAppToast(null), 4000);
861
847
  }, []);
862
848
 
849
+ const handleBlockedTimelineEdit = useCallback(
850
+ (_element: TimelineElement) => {
851
+ const now = Date.now();
852
+ if (now - lastBlockedTimelineToastAtRef.current < 1500) return;
853
+ lastBlockedTimelineToastAtRef.current = now;
854
+ showToast("This clip can’t be moved or resized from the timeline yet.", "info");
855
+ },
856
+ [showToast],
857
+ );
858
+
863
859
  const handleImportFiles = useCallback(
864
860
  async (files: FileList, dir?: string) => {
865
861
  const pid = projectIdRef.current;
@@ -879,20 +875,20 @@ export function StudioApp() {
879
875
  if (res.ok) {
880
876
  const data = await res.json();
881
877
  if (data.skipped?.length) {
882
- showUploadToast(`Skipped (too large): ${data.skipped.join(", ")}`);
878
+ showToast(`Skipped (too large): ${data.skipped.join(", ")}`);
883
879
  }
884
880
  await refreshFileTree();
885
881
  setRefreshKey((k) => k + 1);
886
882
  } else if (res.status === 413) {
887
- showUploadToast("Upload rejected: payload too large");
883
+ showToast("Upload rejected: payload too large");
888
884
  } else {
889
- showUploadToast(`Upload failed (${res.status})`);
885
+ showToast(`Upload failed (${res.status})`);
890
886
  }
891
887
  } catch {
892
- showUploadToast("Upload failed: network error");
888
+ showToast("Upload failed: network error");
893
889
  }
894
890
  },
895
- [refreshFileTree, showUploadToast],
891
+ [refreshFileTree, showToast],
896
892
  );
897
893
 
898
894
  const handleLint = useCallback(async () => {
@@ -1157,6 +1153,7 @@ export function StudioApp() {
1157
1153
  renderClipContent={renderClipContent}
1158
1154
  onMoveElement={handleTimelineElementMove}
1159
1155
  onResizeElement={handleTimelineElementResize}
1156
+ onBlockedEditAttempt={handleBlockedTimelineEdit}
1160
1157
  onCompIdToSrcChange={setCompIdToSrc}
1161
1158
  onCompositionChange={(compPath) => {
1162
1159
  // Sync activeCompPath when user drills down via timeline double-click
@@ -1267,6 +1264,12 @@ export function StudioApp() {
1267
1264
  )}
1268
1265
  </div>
1269
1266
 
1267
+ {timelineElements.length > 0 && !timelineEditorHintDismissed && (
1268
+ <div className="pointer-events-none absolute bottom-5 left-5 z-[140]">
1269
+ <TimelineEditorNotice onDismiss={dismissTimelineEditorHint} />
1270
+ </div>
1271
+ )}
1272
+
1270
1273
  {/* Lint modal */}
1271
1274
  {lintModal !== null && projectId && (
1272
1275
  <LintModal findings={lintModal} projectId={projectId} onClose={() => setLintModal(null)} />
@@ -1306,9 +1309,15 @@ export function StudioApp() {
1306
1309
  </div>
1307
1310
  </div>
1308
1311
  )}
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}
1312
+ {appToast && (
1313
+ <div
1314
+ 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 ${
1315
+ appToast.tone === "error"
1316
+ ? "bg-red-900/90 border-red-700/50 text-red-200"
1317
+ : "bg-neutral-900/95 border-neutral-700/60 text-neutral-100"
1318
+ }`}
1319
+ >
1320
+ {appToast.message}
1312
1321
  </div>
1313
1322
  )}
1314
1323
  </div>
@@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect, memo, type ReactNode } from "
2
2
  import { useMountEffect } from "../../hooks/useMountEffect";
3
3
  import { useTimelinePlayer, PlayerControls, Timeline, usePlayerStore } from "../../player";
4
4
  import type { TimelineElement } from "../../player";
5
+ import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
5
6
  import { NLEPreview } from "./NLEPreview";
6
7
  import { CompositionBreadcrumb, type CompositionLevel } from "./CompositionBreadcrumb";
7
8
 
@@ -36,6 +37,7 @@ interface NLELayoutProps {
36
37
  element: TimelineElement,
37
38
  updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
38
39
  ) => Promise<void> | void;
40
+ onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
39
41
  /** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
40
42
  onCompIdToSrcChange?: (map: Map<string, string>) => void;
41
43
  /** Whether the timeline panel is visible (default: true) */
@@ -61,6 +63,7 @@ export const NLELayout = memo(function NLELayout({
61
63
  renderClipContent,
62
64
  onMoveElement,
63
65
  onResizeElement,
66
+ onBlockedEditAttempt,
64
67
  onCompIdToSrcChange,
65
68
  timelineVisible,
66
69
  onToggleTimeline,
@@ -392,6 +395,7 @@ export const NLELayout = memo(function NLELayout({
392
395
  renderClipContent={renderClipContent}
393
396
  onMoveElement={onMoveElement}
394
397
  onResizeElement={onResizeElement}
398
+ onBlockedEditAttempt={onBlockedEditAttempt}
395
399
  />
396
400
  </div>
397
401
  {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
@@ -0,0 +1,156 @@
1
+ import { TIMELINE_TOGGLE_SHORTCUT_LABEL } from "../../utils/timelineDiscovery";
2
+
3
+ interface TimelineEditorNoticeProps {
4
+ onDismiss: () => void;
5
+ }
6
+
7
+ export function TimelineEditorNotice({ onDismiss }: TimelineEditorNoticeProps) {
8
+ return (
9
+ <aside
10
+ aria-live="polite"
11
+ className="pointer-events-none relative w-[320px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-white/10 bg-[#0f1115]/88 text-neutral-100 shadow-[0_18px_40px_rgba(0,0,0,0.3),0_4px_14px_rgba(0,0,0,0.18)] backdrop-blur-xl"
12
+ >
13
+ <style>{`
14
+ @keyframes hfTimelineNoticeClipNudge {
15
+ 0%, 100% { transform: translate3d(0, 0, 0); }
16
+ 20% { transform: translate3d(0, 0, 0); }
17
+ 52% { transform: translate3d(12px, 0, 0); }
18
+ 72% { transform: translate3d(12px, 0, 0); }
19
+ 100% { transform: translate3d(0, 0, 0); }
20
+ }
21
+
22
+ @keyframes hfTimelineNoticePlayheadSweep {
23
+ 0% { transform: translateX(0); opacity: 0; }
24
+ 10% { opacity: 1; }
25
+ 75% { opacity: 1; }
26
+ 100% { transform: translateX(218px); opacity: 0; }
27
+ }
28
+
29
+ @media (prefers-reduced-motion: reduce) {
30
+ .hf-timeline-notice-clip,
31
+ .hf-timeline-notice-playhead {
32
+ animation: none !important;
33
+ }
34
+ }
35
+ `}</style>
36
+
37
+ <button
38
+ type="button"
39
+ onClick={onDismiss}
40
+ aria-label="Dismiss timeline editor notice"
41
+ className="pointer-events-auto absolute right-3 top-3 z-10 flex h-7 w-7 items-center justify-center rounded-lg text-neutral-500 transition-colors duration-150 hover:bg-white/[0.06] hover:text-neutral-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-studio-accent/50"
42
+ >
43
+ <svg
44
+ width="11"
45
+ height="11"
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth="2.25"
50
+ strokeLinecap="round"
51
+ aria-hidden="true"
52
+ >
53
+ <line x1="18" y1="6" x2="6" y2="18" />
54
+ <line x1="6" y1="6" x2="18" y2="18" />
55
+ </svg>
56
+ </button>
57
+
58
+ <div className="flex items-start gap-3 px-4 py-3.5">
59
+ <div className="min-w-0 flex-1">
60
+ <div
61
+ aria-hidden="true"
62
+ className="mb-3 overflow-hidden rounded-[14px] bg-[#0d1117] p-2.5"
63
+ >
64
+ <div className="relative overflow-hidden rounded-[11px] bg-[#0f141c] px-2.5 pb-2 pt-1.5">
65
+ <div className="mb-1.5 flex items-center justify-between pl-6 pr-1 text-[8px] font-medium text-[#7f8796]">
66
+ <span>0:00</span>
67
+ <span>0:05</span>
68
+ <span>0:10</span>
69
+ </div>
70
+
71
+ <div className="pointer-events-none absolute inset-x-0 top-[18px] h-px bg-white/[0.04]" />
72
+ <div
73
+ className="hf-timeline-notice-playhead pointer-events-none absolute left-[31px] top-[18px] h-[70px] w-0"
74
+ style={{
75
+ animation:
76
+ "hfTimelineNoticePlayheadSweep 2.8s cubic-bezier(0.4, 0, 0.2, 1) infinite",
77
+ }}
78
+ >
79
+ <div
80
+ className="absolute top-0 bottom-0"
81
+ style={{
82
+ left: "50%",
83
+ width: 2,
84
+ marginLeft: -1,
85
+ background: "var(--hf-accent, #3CE6AC)",
86
+ boxShadow: "0 0 8px rgba(60,230,172,0.5)",
87
+ }}
88
+ />
89
+ <div
90
+ className="absolute"
91
+ style={{ left: "50%", top: 0, transform: "translateX(-50%)" }}
92
+ >
93
+ <div
94
+ style={{
95
+ width: 0,
96
+ height: 0,
97
+ borderLeft: "6px solid transparent",
98
+ borderRight: "6px solid transparent",
99
+ borderTop: "8px solid var(--hf-accent, #3CE6AC)",
100
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.6))",
101
+ }}
102
+ />
103
+ </div>
104
+ </div>
105
+
106
+ <div className="flex flex-col gap-1.5">
107
+ {[0, 1, 2].map((trackIndex) => (
108
+ <div
109
+ key={trackIndex}
110
+ className="relative h-6 overflow-hidden rounded-[10px] bg-white/[0.035]"
111
+ >
112
+ <div className="absolute inset-y-0 left-[24px] w-px bg-white/[0.035]" />
113
+ <div className="absolute inset-y-0 left-[100px] w-px bg-white/[0.035]" />
114
+ <div className="absolute inset-y-0 left-[176px] w-px bg-white/[0.035]" />
115
+ </div>
116
+ ))}
117
+ </div>
118
+
119
+ <div className="pointer-events-none absolute inset-x-0 top-[21px] h-[70px]">
120
+ <div className="absolute left-[34px] top-[3px] h-[18px] w-[56px] rounded-[9px] bg-white/[0.07]" />
121
+ <div
122
+ className="hf-timeline-notice-clip absolute left-[82px] top-[27px] h-[18px] w-[110px] rounded-[9px] bg-studio-accent/18 ring-1 ring-inset ring-studio-accent/28"
123
+ style={{
124
+ animation:
125
+ "hfTimelineNoticeClipNudge 2.8s cubic-bezier(0.4, 0, 0.2, 1) infinite",
126
+ }}
127
+ />
128
+ <div className="absolute left-[52px] top-[51px] h-[18px] w-[72px] rounded-[9px] bg-white/[0.07]" />
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="min-w-0 pr-9">
134
+ <p className="text-[12px] font-semibold leading-none tracking-tight text-neutral-100">
135
+ Timeline editing is on
136
+ </p>
137
+ <p className="mt-1.5 text-[12px] leading-5 text-neutral-300">
138
+ Drag clips to move timing, use{" "}
139
+ <span className="font-mono text-[11px] text-studio-accent">Shift</span> + click to
140
+ edit a full clip range, and watch for resize handles only on clips Studio can patch
141
+ safely. Toggle the timeline with{" "}
142
+ <span className="rounded-md border border-white/8 bg-white/[0.04] px-1.5 py-0.5 font-mono text-[11px] text-studio-accent">
143
+ {TIMELINE_TOGGLE_SHORTCUT_LABEL}
144
+ </span>
145
+ .
146
+ </p>
147
+ </div>
148
+
149
+ <div className="mt-2 text-[10px] leading-none text-neutral-500">
150
+ Dismiss once and it stays hidden.
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </aside>
155
+ );
156
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveCompositionPreviewScale } from "./CompositionsTab";
3
+
4
+ describe("resolveCompositionPreviewScale", () => {
5
+ it("scales a 16:9 stage to fit the composition card", () => {
6
+ expect(
7
+ resolveCompositionPreviewScale({
8
+ cardWidth: 80,
9
+ cardHeight: 45,
10
+ stageWidth: 1920,
11
+ stageHeight: 1080,
12
+ }),
13
+ ).toBeCloseTo(80 / 1920);
14
+ });
15
+
16
+ it("scales non-16:9 stages against their actual dimensions", () => {
17
+ expect(
18
+ resolveCompositionPreviewScale({
19
+ cardWidth: 80,
20
+ cardHeight: 45,
21
+ stageWidth: 1280,
22
+ stageHeight: 720,
23
+ }),
24
+ ).toBeCloseTo(80 / 1280);
25
+ });
26
+
27
+ it("falls back to the default stage when dimensions are invalid", () => {
28
+ expect(
29
+ resolveCompositionPreviewScale({
30
+ cardWidth: 80,
31
+ cardHeight: 45,
32
+ stageWidth: 0,
33
+ stageHeight: Number.NaN,
34
+ }),
35
+ ).toBeCloseTo(80 / 1920);
36
+ });
37
+ });
@@ -7,6 +7,27 @@ interface CompositionsTabProps {
7
7
  onSelect: (comp: string) => void;
8
8
  }
9
9
 
10
+ const DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
11
+
12
+ export function resolveCompositionPreviewScale(input: {
13
+ cardWidth: number;
14
+ cardHeight: number;
15
+ stageWidth: number;
16
+ stageHeight: number;
17
+ }): number {
18
+ const safeStageWidth =
19
+ Number.isFinite(input.stageWidth) && input.stageWidth > 0
20
+ ? input.stageWidth
21
+ : DEFAULT_PREVIEW_STAGE.width;
22
+ const safeStageHeight =
23
+ Number.isFinite(input.stageHeight) && input.stageHeight > 0
24
+ ? input.stageHeight
25
+ : DEFAULT_PREVIEW_STAGE.height;
26
+ const scaleX = input.cardWidth / safeStageWidth;
27
+ const scaleY = input.cardHeight / safeStageHeight;
28
+ return Math.min(scaleX, scaleY);
29
+ }
30
+
10
31
  function CompCard({
11
32
  projectId,
12
33
  comp,
@@ -19,6 +40,7 @@ function CompCard({
19
40
  onSelect: () => void;
20
41
  }) {
21
42
  const [hovered, setHovered] = useState(false);
43
+ const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
22
44
  const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
23
45
  const handleEnter = () => {
24
46
  hoverTimer.current = setTimeout(() => setHovered(true), 300);
@@ -33,6 +55,12 @@ function CompCard({
33
55
  const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
34
56
  const thumbnailUrl = `/api/projects/${projectId}/thumbnail/${comp}?t=2`;
35
57
  const previewUrl = `/api/projects/${projectId}/preview/comp/${comp}`;
58
+ const previewScale = resolveCompositionPreviewScale({
59
+ cardWidth: 80,
60
+ cardHeight: 45,
61
+ stageWidth: stageSize.width,
62
+ stageHeight: stageSize.height,
63
+ });
36
64
 
37
65
  return (
38
66
  <div
@@ -51,10 +79,25 @@ function CompCard({
51
79
  <iframe
52
80
  src={previewUrl}
53
81
  sandbox="allow-scripts allow-same-origin"
54
- className="absolute inset-0 w-[1920px] h-[1080px] border-none pointer-events-none"
82
+ className="absolute left-0 top-0 border-none pointer-events-none"
55
83
  style={{
56
84
  transformOrigin: "0 0",
57
- transform: `scale(${80 / 1920})`,
85
+ width: stageSize.width,
86
+ height: stageSize.height,
87
+ transform: `scale(${previewScale})`,
88
+ }}
89
+ onLoad={(e) => {
90
+ try {
91
+ const iframe = e.currentTarget;
92
+ const root = iframe.contentDocument?.querySelector("[data-composition-id]");
93
+ const width =
94
+ Number(root?.getAttribute("data-width")) || DEFAULT_PREVIEW_STAGE.width;
95
+ const height =
96
+ Number(root?.getAttribute("data-height")) || DEFAULT_PREVIEW_STAGE.height;
97
+ setStageSize({ width, height });
98
+ } catch {
99
+ setStageSize(DEFAULT_PREVIEW_STAGE);
100
+ }
58
101
  }}
59
102
  tabIndex={-1}
60
103
  />
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  generateTicks,
4
+ getTimelineCanvasHeight,
4
5
  getTimelinePlayheadLeft,
5
6
  getTimelineScrollLeftForZoomTransition,
6
7
  shouldAutoScrollTimeline,
@@ -151,3 +152,13 @@ describe("getTimelinePlayheadLeft", () => {
151
152
  expect(getTimelinePlayheadLeft(4, Number.NaN)).toBe(32);
152
153
  });
153
154
  });
155
+
156
+ describe("getTimelineCanvasHeight", () => {
157
+ it("includes bottom scroll buffer below the last track", () => {
158
+ expect(getTimelineCanvasHeight(3)).toBeGreaterThan(24 + 3 * 72);
159
+ });
160
+
161
+ it("still keeps ruler space when there are no tracks", () => {
162
+ expect(getTimelineCanvasHeight(0)).toBeGreaterThan(24);
163
+ });
164
+ });