@hyperframes/studio 0.4.16 → 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.
- package/dist/assets/index-JZr8f8y8.js +115 -0
- package/dist/assets/index-kT65pCwW.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +47 -38
- package/src/components/nle/NLELayout.tsx +4 -0
- package/src/components/nle/TimelineEditorNotice.tsx +156 -0
- package/src/components/sidebar/CompositionsTab.test.ts +37 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/player/components/Timeline.test.ts +11 -0
- package/src/player/components/Timeline.tsx +105 -17
- package/src/player/components/TimelineClip.tsx +1 -1
- package/src/player/components/timelineEditing.test.ts +149 -0
- package/src/player/components/timelineEditing.ts +45 -6
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/dist/assets/index-CVm-zeM9.css +0 -1
- package/dist/assets/index-RzXlAX2g.js +0 -93
|
@@ -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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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.
|
|
36
|
-
"@hyperframes/player": "0.4.
|
|
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.
|
|
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 [
|
|
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
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
883
|
+
showToast("Upload rejected: payload too large");
|
|
888
884
|
} else {
|
|
889
|
-
|
|
885
|
+
showToast(`Upload failed (${res.status})`);
|
|
890
886
|
}
|
|
891
887
|
} catch {
|
|
892
|
-
|
|
888
|
+
showToast("Upload failed: network error");
|
|
893
889
|
}
|
|
894
890
|
},
|
|
895
|
-
[refreshFileTree,
|
|
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
|
-
{
|
|
1310
|
-
<div
|
|
1311
|
-
{
|
|
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
|
|
82
|
+
className="absolute left-0 top-0 border-none pointer-events-none"
|
|
55
83
|
style={{
|
|
56
84
|
transformOrigin: "0 0",
|
|
57
|
-
|
|
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
|
+
});
|