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