@designbasekorea/ui-wc 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import './db-button.js';
2
+ import './db-select.js';
3
+ import './db-input.js';
4
+ import { EditorKeyframe } from './media-editor-model.js';
5
+ export { KeyframeEasing, evaluateKeyframes } from './media-editor-model.js';
6
+
7
+ interface KeyframeEditDetail {
8
+ keyframes: EditorKeyframe[];
9
+ action: 'add' | 'delete' | 'move' | 'easing' | 'bezier' | 'value';
10
+ keyframeId: string;
11
+ }
12
+ declare class DbKeyframeEditor extends HTMLElement {
13
+ static get observedAttributes(): string[];
14
+ private _keys;
15
+ private viewport?;
16
+ private chart?;
17
+ private toolbar?;
18
+ private inspector?;
19
+ private cursor?;
20
+ private unsubscribe?;
21
+ private drag?;
22
+ private raf;
23
+ private num;
24
+ get duration(): number;
25
+ set duration(n: number);
26
+ get fps(): number;
27
+ set fps(n: number);
28
+ get zoom(): number;
29
+ set zoom(n: number);
30
+ get currentTime(): number;
31
+ set currentTime(n: number);
32
+ get min(): number;
33
+ set min(n: number);
34
+ get max(): number;
35
+ set max(n: number);
36
+ get disabled(): boolean;
37
+ set disabled(n: boolean);
38
+ get selectedKeyframeId(): string;
39
+ set selectedKeyframeId(id: string);
40
+ get keyframes(): EditorKeyframe[];
41
+ set keyframes(value: EditorKeyframe[]);
42
+ private get scale();
43
+ connectedCallback(): void;
44
+ disconnectedCallback(): void;
45
+ attributeChangedCallback(name: string): void;
46
+ private emit;
47
+ private select;
48
+ private seek;
49
+ private plotBounds;
50
+ private y;
51
+ private syncCursor;
52
+ private svg;
53
+ private render;
54
+ addKeyframe(time?: number, value?: number): void;
55
+ private deleteKey;
56
+ private navigate;
57
+ private onActionClick;
58
+ private changeKey;
59
+ private fieldInput;
60
+ private field;
61
+ private setTangent;
62
+ restoreHandles(): void;
63
+ private point;
64
+ private double;
65
+ private down;
66
+ private update;
67
+ private move;
68
+ private autoScroll;
69
+ private finish;
70
+ private up;
71
+ private cancel;
72
+ private key;
73
+ }
74
+
75
+ export { DbKeyframeEditor, EditorKeyframe };
76
+ export type { KeyframeEditDetail };
@@ -0,0 +1 @@
1
+ import"./db-button.js";import"./db-select.js";import"./db-input.js";import{subscribeDbLocale as e,t}from"../core/i18n.js";import{applyKeyframeEasing as i,clamp as s,editorEditable as n,keyframeTangents as a,frameTime as r,finite as d,emitEditor as h,editorButton as o,keyframeSegment as l,evaluateKeyframes as m}from"./media-editor-model.js";const c=["linear","hold","ease-in","ease-out","ease-in-out","bezier"];class DbKeyframeEditor extends HTMLElement{constructor(){super(...arguments),this._keys=[],this.raf=0,this.onActionClick=e=>{if(!this.disabled)switch(e.target.dataset.action){case"add":this.addKeyframe();break;case"delete":this.deleteKey();break;case"restore":this.restoreHandles();break;case"previous":this.navigate(-1);break;case"next":this.navigate(1)}},this.fieldInput=e=>{e.target!==this&&e.target.dataset.field&&e.stopImmediatePropagation()},this.field=e=>{const t=e.target.dataset.field;if(!t)return;if(e.stopImmediatePropagation(),this.disabled)return;const s=this._keys.find(e=>e.id===this.selectedKeyframeId);if(!s)return;const n=e.detail?.value;"easing"===t&&c.includes(n)?(this._keys=i(this._keys,s.id,n),this.render(),this.emit("easing",s.id)):"time"!==t&&"value"!==t||""===n||!Number.isFinite(Number(n))||this.changeKey(s.id,"time"===t?Number(n):s.time,"value"===t?Number(n):s.value,"value")},this.double=e=>{if(this.disabled||!(e.target instanceof Element)||!e.target.closest(".db-kf__svg")||e.target.closest("[data-key],[data-handle]"))return;const t=this.point(e.clientX,e.clientY);this.addKeyframe(t.time,t.value)},this.down=e=>{if(this.drag)return;if(this.disabled||0!==e.button||!(e.target instanceof Element))return;const t=e.target.closest("[data-key],[data-handle]");if(!t)return void(e.target.closest(".db-kf__svg")&&"touch"!==e.pointerType&&this.seek(this.point(e.clientX,e.clientY).time));e.preventDefault();const i=t.dataset.key||t.dataset.owner;this.select(i),this.viewport?.focus({preventScroll:!0}),this.drag={id:e.pointerId,keyId:i,handle:t.hasAttribute("data-handle")?Number(t.dataset.handle):void 0,x:e.clientX,y:e.clientY,originX:e.clientX,originY:e.clientY,scroll:this.viewport.scrollLeft,before:this.keyframes,changed:!1,independent:e.metaKey||e.ctrlKey,viewMin:this.plotBounds().min,viewMax:this.plotBounds().max},this.setPointerCapture(e.pointerId),this.raf=requestAnimationFrame(this.autoScroll)},this.move=e=>{if(this.drag?.id!==e.pointerId)return;this.drag.x=e.clientX,this.drag.y=e.clientY;const t=e.metaKey||e.ctrlKey;t&&!this.drag.independent&&(this.drag.freeBase=this.keyframes),this.drag.independent=t,this.update()},this.autoScroll=()=>{if(!this.drag||!this.viewport)return;const e=this.viewport.getBoundingClientRect(),t=this.drag.x,i=this.viewport.scrollLeft;this.viewport.scrollLeft+=t>e.right-40?s((t-e.right+40)/3,0,14):t<e.left+40?-s((e.left+40-t)/3,0,14):0,i!==this.viewport.scrollLeft&&this.update(),this.raf=requestAnimationFrame(this.autoScroll)},this.up=e=>{e.pointerId===this.drag?.id&&this.finish()},this.cancel=()=>this.finish(!0),this.key=e=>{if(this.disabled||n(e.target)||e.metaKey||e.ctrlKey||e.altKey)return;if("Escape"===e.key&&this.drag)return e.preventDefault(),void this.finish(!0);const t=e.target,i=this._keys.find(e=>e.id===(t.dataset.key||t.dataset.owner||this.selectedKeyframeId));if("Enter"===e.key&&t.dataset.key)return e.preventDefault(),void this.select(t.dataset.key);if("Delete"===e.key||"Backspace"===e.key)return e.preventDefault(),void this.deleteKey();if("h"===e.key.toLowerCase())return e.preventDefault(),void this.restoreHandles();if("k"===e.key.toLowerCase())return e.preventDefault(),void this.addKeyframe();if(i&&["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){e.preventDefault();const s=e.shiftKey?10:1;if(t.hasAttribute("data-handle")){const n=Number(t.dataset.handle),r=a(this._keys,this._keys.indexOf(i)),d=0===n?r.outgoing:r.incoming;if(!d)return;const h={time:d.time+("ArrowLeft"===e.key?-s/this.fps:"ArrowRight"===e.key?s/this.fps:0),value:d.value+("ArrowUp"===e.key?s:"ArrowDown"===e.key?-s:0)};this.setTangent(this.keyframes,i.id,n,h,!1),this.render(),this.emit("bezier",i.id),this.querySelector(`[data-handle="${n}"]`)?.focus()}else this.select(i.id),this.changeKey(i.id,i.time+("ArrowLeft"===e.key?-s/this.fps:"ArrowRight"===e.key?s/this.fps:0),i.value+("ArrowUp"===e.key?s:"ArrowDown"===e.key?-s:0),"move")}}}static get observedAttributes(){return["duration","fps","current-time","zoom","min","max","disabled","selected-keyframe-id","label"]}num(e,t){const i=this.getAttribute(e);return i?.trim()&&Number.isFinite(Number(i))?Number(i):t}get duration(){return s(this.num("duration",10),1/this.fps,86400)}set duration(e){this.setAttribute("duration",String(e))}get fps(){return s(Math.round(this.num("fps",30)),1,120)}set fps(e){this.setAttribute("fps",String(e))}get zoom(){return s(this.num("zoom",100),10,800)}set zoom(e){this.setAttribute("zoom",String(e))}get currentTime(){return r(s(this.num("current-time",0),0,this.duration),this.fps)}set currentTime(e){this.setAttribute("current-time",String(e))}get min(){return this.num("min",0)}set min(e){this.setAttribute("min",String(e))}get max(){return Math.max(this.min+.001,this.num("max",100))}set max(e){this.setAttribute("max",String(e))}get disabled(){return this.hasAttribute("disabled")}set disabled(e){this.toggleAttribute("disabled",e)}get selectedKeyframeId(){return this.getAttribute("selected-keyframe-id")||""}set selectedKeyframeId(e){this.setAttribute("selected-keyframe-id",e)}get keyframes(){return this._keys.map(e=>({...e,...e.inTangent?{inTangent:{...e.inTangent}}:{},...e.outTangent?{outTangent:{...e.outTangent}}:{},bezier:e.bezier?[...e.bezier]:void 0}))}set keyframes(e){const t=new Set,i=new Set;this._keys=(Array.isArray(e)?e:[]).filter(e=>e&&"string"==typeof e.id&&!t.has(e.id)&&!!t.add(e.id)).map(e=>({...e,time:r(s(d(e.time,0),0,this.duration),this.fps),value:s(d(e.value,this.min),this.min,this.max),inTangent:e.inTangent?{time:-Math.min(this.duration,Math.abs(d(e.inTangent.time,0))),value:s(d(e.inTangent.value,0),4*-(this.max-this.min),4*(this.max-this.min))}:void 0,outTangent:e.outTangent?{time:Math.min(this.duration,Math.abs(d(e.outTangent.time,0))),value:s(d(e.outTangent.value,0),4*-(this.max-this.min),4*(this.max-this.min))}:void 0,easing:c.includes(e.easing)?e.easing:"linear",bezier:Array.isArray(e.bezier)&&4===e.bezier.length?e.bezier.map(e=>s(d(e,0),0,1)):void 0})).filter(e=>!i.has(e.time)&&!!i.add(e.time)).sort((e,t)=>e.time-t.time),this.render()}get scale(){return.8*this.zoom}connectedCallback(){this.classList.add("designbase-wc-keyframe-editor"),this.setAttribute("role","region"),this.viewport||(this.toolbar=document.createElement("div"),this.toolbar.className="db-kf__toolbar",this.viewport=document.createElement("div"),this.viewport.className="db-kf__viewport",this.viewport.tabIndex=0,this.chart=document.createElement("div"),this.chart.className="db-kf__chart",this.viewport.append(this.chart),this.inspector=document.createElement("div"),this.inspector.className="db-kf__inspector",this.append(this.toolbar,this.viewport,this.inspector)),this.addEventListener("pointerdown",this.down),this.addEventListener("pointermove",this.move),this.addEventListener("pointerup",this.up),this.addEventListener("pointercancel",this.cancel),this.addEventListener("lostpointercapture",this.cancel),this.addEventListener("keydown",this.key),this.addEventListener("dblclick",this.double),this.addEventListener("db-click",this.onActionClick),this.addEventListener("db-change",this.field,!0),this.addEventListener("db-input",this.fieldInput,!0),this.unsubscribe=e(()=>this.render()),this.render()}disconnectedCallback(){this.finish(!0),this.unsubscribe?.(),this.removeEventListener("pointerdown",this.down),this.removeEventListener("pointermove",this.move),this.removeEventListener("pointerup",this.up),this.removeEventListener("pointercancel",this.cancel),this.removeEventListener("lostpointercapture",this.cancel),this.removeEventListener("keydown",this.key),this.removeEventListener("dblclick",this.double),this.removeEventListener("db-click",this.onActionClick),this.removeEventListener("db-change",this.field,!0),this.removeEventListener("db-input",this.fieldInput,!0)}attributeChangedCallback(e){"current-time"===e?this.syncCursor():this.render()}emit(e,t){h(this,"db-change",{keyframes:this.keyframes,action:e,keyframeId:t})}select(e){this.selectedKeyframeId!==e&&(this.selectedKeyframeId=e,h(this,"db-selection-change",{keyframeId:e}))}seek(e){this.currentTime=r(s(e,0,this.duration),this.fps),h(this,"db-seek",{time:this.currentTime})}plotBounds(){if(this.drag)return{min:this.drag.viewMin,max:this.drag.viewMax};let e=this.min,t=this.max;this._keys.forEach((i,s)=>{const n=a(this._keys,s);for(const s of[n.incoming,n.outgoing])s&&(e=Math.min(e,i.value+s.value),t=Math.max(t,i.value+s.value))});const i=.04*(t-e);return{min:e<this.min?e-i:e,max:t>this.max?t+i:t}}y(e){const t=this.plotBounds();return 208-(e-t.min)/(t.max-t.min)*176}syncCursor(){if(this.cursor){const e=40+this.currentTime*this.scale;this.cursor.setAttribute("x1",String(e)),this.cursor.setAttribute("x2",String(e))}}svg(e,t={},i){const s=document.createElementNS("http://www.w3.org/2000/svg",e);for(const[e,i]of Object.entries(t))s.setAttribute(e,String(i));return i&&(s.textContent=i),s}render(){if(!this.chart||!this.toolbar||!this.inspector)return;const e=this.ownerDocument.activeElement?.dataset.key,i=this._keys.find(e=>e.id===this.selectedKeyframeId);this.setAttribute("aria-label",this.getAttribute("label")||t("media.keyframes"));const s=document.createElement("strong");s.textContent=this.getAttribute("label")||t("media.keyframes");const n=document.createElement("div");n.className="db-kf__actions";for(const[e,s,a]of[["previous","chevron-left","media.previousKey"],["add","plus","media.addKey"],["next","chevron-right","media.nextKey"],["delete","trash","media.deleteKey"],["restore","refresh","media.restoreHandles"]]){const r=o(t(a),s,e);r.toggleAttribute("disabled",this.disabled||("delete"===e||"restore"===e)&&!i),n.append(r)}this.toolbar.replaceChildren(s,n);const r=Math.max(320,80+this.duration*this.scale),d=this.svg("svg",{width:r,height:240,viewBox:`0 0 ${r} 240`,class:"db-kf__svg","aria-label":t("media.curve")});for(let e=0;e<=4;e++){const t=this.plotBounds(),i=t.min+(t.max-t.min)*e/4,s=this.y(i);d.append(this.svg("line",{x1:40,y1:s,x2:r-40,y2:s,class:"db-kf__grid"}),this.svg("text",{x:32,y:s+4,"text-anchor":"end",class:"db-kf__label"},String(Math.round(100*i)/100)))}const h=[.1,.5,1,2,5,10,30,60,300,600,3600].find(e=>e*this.scale>=64&&this.duration/e<=200)||3600;for(let e=0;e<=this.duration;e+=h){const t=40+e*this.scale;d.append(this.svg("line",{x1:t,y1:24,x2:t,y2:208,class:"db-kf__grid"}),this.svg("text",{x:t,y:230,"text-anchor":"middle",class:"db-kf__label"},Math.round(100*e)/100+"s"))}for(let e=0;e<this._keys.length-1;e++){const t=this._keys[e],i=this._keys[e+1],s=40+t.time*this.scale,n=40+i.time*this.scale,a=this.y(t.value),r=this.y(i.value),h=l(this._keys,e);d.append(this.svg("path",{d:"hold"===t.easing?`M${s},${a}H${n}V${r}`:`M${s},${a}C${40+h.time1*this.scale},${this.y(h.value1)} ${40+h.time2*this.scale},${this.y(h.value2)} ${n},${r}`,class:"db-kf__curve"}))}this.cursor=this.svg("line",{y1:24,y2:208,class:"db-kf__playhead","pointer-events":"none"}),d.append(this.cursor);for(const e of this._keys){const s=40+e.time*this.scale,n=this.y(e.value),a=this.svg("g",{class:"db-kf__point","data-key":e.id,"data-selected":String(e.id===i?.id),role:"button",tabindex:0,"aria-label":`${t("media.keyframe")} ${e.time.toFixed(2)}s, ${e.value.toFixed(1)}`,"aria-pressed":String(e.id===i?.id),"aria-disabled":String(this.disabled)});a.append(this.svg("circle",{cx:s,cy:n,r:14,fill:"transparent"})),a.append(this.svg("rect",{x:s-3,y:n-3,width:6,height:6,transform:`rotate(45 ${s} ${n})`,class:"db-kf__key"})),d.append(a)}if(i){const e=a(this._keys,this._keys.indexOf(i)),s=40+i.time*this.scale,n=this.y(i.value);for(const[a,r,h]of[["out",0,e.outgoing],["in",1,e.incoming]]){if(!h)continue;let e=h.time*this.scale,o=this.y(i.value+h.value)-n;const l=Math.hypot(e,o),m=l<22;if(m){const t=l>.01?24/l:0;e=t?e*t:"in"===a?-24:24,o=t?o*t:0}const c=s+e,u=n+o;d.append(this.svg("line",{x1:s,y1:n,x2:c,y2:u,class:"db-kf__tangent","data-proxy":String(m)}));const f=this.svg("g",{"data-handle":r,"data-side":a,"data-owner":i.id,"data-collapsed":String(m),class:"db-kf__handle-target",role:"slider",tabindex:0,"aria-label":t("in"===a?"media.inHandle":"media.outHandle"),"aria-valuemin":-this.duration,"aria-valuemax":this.duration,"aria-valuenow":h.time,"aria-valuetext":`${h.time.toFixed(3)}s, ${h.value.toFixed(3)}`});f.append(this.svg("circle",{cx:c,cy:u,r:10,fill:"transparent"}),this.svg("circle",{cx:c,cy:u,r:3,class:"db-kf__handle"})),d.append(f)}}this._keys.length||d.append(this.svg("text",{x:60,y:120,class:"db-kf__label"},t("media.emptyKeys"))),this.chart.replaceChildren(d),this.syncCursor();const m=document.createDocumentFragment();if(i){for(const e of["time","value"]){const s=document.createElement("db-input");s.setAttribute("size","s"),s.setAttribute("type","number"),s.setAttribute("label",t("time"===e?"media.time":"media.value")),s.setAttribute("value",String(Number(i[e].toFixed("time"===e?4:3)))),s.setAttribute("step","time"===e?String(1/this.fps):"1"),s.setAttribute("min",String("time"===e?0:this.min)),s.setAttribute("max",String("time"===e?this.duration:this.max)),s.dataset.field=e,s.toggleAttribute("disabled",this.disabled),m.append(s)}const e=document.createElement("db-select");e.setAttribute("size","s"),e.setAttribute("show-clear-button","false"),e.setAttribute("label",t("media.interpolation")),e.setAttribute("options",JSON.stringify(c.map(e=>({value:e,label:t("media."+e)})))),e.setAttribute("value",i.easing||"linear"),e.dataset.field="easing",e.toggleAttribute("disabled",this.disabled),m.append(e)}else{const e=document.createElement("span");e.textContent=t("media.selectKey"),m.append(e)}const u=document.createElement("span");u.className="db-kf__hint",u.textContent=t("media.tangentHelp"),m.append(u),this.inspector.replaceChildren(m),e&&!this.drag&&this.querySelector(`[data-key="${CSS.escape(e)}"]`)?.focus()}addKeyframe(e=this.currentTime,t=m(this._keys,e)){if(this.disabled)return;e=r(s(e,0,this.duration),this.fps);const i=this._keys.find(t=>Math.abs(t.time-e)<.5/this.fps);if(i)return void this.select(i.id);let n="key-"+Math.round(e*this.fps);for(;this._keys.some(e=>e.id===n);)n+="-1";this._keys.push({id:n,time:e,value:s(t,this.min,this.max),easing:"linear"}),this._keys.sort((e,t)=>e.time-t.time),this.select(n),this.render(),this.emit("add",n)}deleteKey(){const e=this.selectedKeyframeId;if(this.disabled||!this._keys.some(t=>t.id===e))return;const t=this._keys.findIndex(t=>t.id===e);this._keys=this._keys.filter(t=>t.id!==e),this.select(this._keys[Math.min(t,this._keys.length-1)]?.id||""),this.render(),this.emit("delete",e)}navigate(e){const t=(e<0?[...this._keys].reverse():this._keys).find(t=>e<0?t.time<this.currentTime-.5/this.fps:t.time>this.currentTime+.5/this.fps);t&&(this.select(t.id),this.seek(t.time),this.viewport&&(this.viewport.scrollLeft=Math.max(0,t.time*this.scale-this.viewport.clientWidth/2)))}changeKey(e,t,i,n,a=!0){const d=this._keys,o=d.findIndex(t=>t.id===e);o<0||(t=r(s(t,o?d[o-1].time+1/this.fps:0,o<d.length-1?d[o+1].time-1/this.fps:this.duration),this.fps),this._keys=d.map(n=>n.id===e?{...n,time:t,value:s(i,this.min,this.max)}:n),this.render(),a?this.emit(n,e):h(this,"db-input",{keyframes:this.keyframes,keyframeId:e,action:n}))}setTangent(e,t,i,n,r){const d=e.findIndex(e=>e.id===t),h=e[d],o=e[d-1],l=e[d+1];if(!h)return;const m=a(e,d),c=o?h.time-o.time:1/0,u=l?l.time-h.time:1/0,f=(r?0===i?u:c:Math.min(c,u))/2,p=0===i?1:-1,g=p*s(p*n.time,0,f),y=s(n.value,4*-(this.max-this.min),4*(this.max-this.min));let v=m.incoming,b=m.outgoing;r?0===i?b={time:g,value:y}:v={time:g,value:y}:(b=l?{time:Math.abs(g),value:p*y}:void 0,v=o?{time:-Math.abs(g),value:-p*y}:void 0),this._keys=e.map((e,t)=>t===d?{...e,inTangent:v,outTangent:b,tangentMode:r?"free":"aligned",easing:!l||0!==i&&r?e.easing:"bezier"}:t!==d-1||!v||1!==i&&r||"hold"!==e.easing?e:{...e,easing:"bezier"})}restoreHandles(){if(this.disabled)return;const e=this._keys.findIndex(e=>e.id===this.selectedKeyframeId),t=this._keys[e],i=this._keys[e-1],s=this._keys[e+1];if(!t||!i&&!s)return;const n=Math.min(i?t.time-i.time:1/0,s?s.time-t.time:1/0)/3,a=i?(t.value-i.value)/(t.time-i.time):void 0,r=s?(s.value-t.value)/(s.time-t.time):void 0,d=void 0!==a&&void 0!==r?a*r<=0?0:(s.value-i.value)/(s.time-i.time):a??r??0;this.setTangent(this.keyframes,t.id,s?0:1,{time:s?n:-n,value:d*(s?n:-n)},!1),this.render(),this.emit("bezier",t.id)}point(e,t){const i=this.chart.getBoundingClientRect();return{time:(e-i.left-40)/this.scale,value:this.plotBounds().max-(t-i.top-32)/176*(this.plotBounds().max-this.plotBounds().min)}}update(){const e=this.drag;if(!e)return;const t=e.before.find(t=>t.id===e.keyId);if(void 0!==e.handle){const i=this.point(e.x,e.y);this.setTangent(e.independent&&e.freeBase||e.before,e.keyId,e.handle,{time:i.time-t.time,value:i.value-t.value},e.independent),this.render(),h(this,"db-input",{keyframes:this.keyframes,keyframeId:t.id,action:"bezier"})}else this.changeKey(t.id,t.time+(e.x-e.originX+this.viewport.scrollLeft-e.scroll)/this.scale,t.value-(e.y-e.originY)/176*(e.viewMax-e.viewMin),"move",!1);e.changed=JSON.stringify(this._keys)!==JSON.stringify(e.before)}finish(e=!1){const t=this.drag;t&&(cancelAnimationFrame(this.raf),this.drag=void 0,this.hasPointerCapture(t.id)&&this.releasePointerCapture(t.id),e?(this._keys=t.before,h(this,"db-input",{keyframes:this.keyframes,cancelled:!0})):t.changed&&this.emit(void 0!==t.handle?"bezier":"move",t.keyId),this.render(),this.querySelector(`[data-key="${CSS.escape(t.keyId)}"]`)?.focus())}}customElements.get("db-keyframe-editor")||customElements.define("db-keyframe-editor",DbKeyframeEditor);export{DbKeyframeEditor,m as evaluateKeyframes};
@@ -0,0 +1,89 @@
1
+ import './db-button.js';
2
+ import { VideoTrack, VideoClip, TimelineMarker } from './media-editor-model.js';
3
+ export { VideoClipEffect } from './media-editor-model.js';
4
+
5
+ interface TimelineEditDetail {
6
+ clips: VideoClip[];
7
+ action: 'move' | 'trim-start' | 'trim-end' | 'fade-in' | 'fade-out' | 'delete' | 'split';
8
+ clipId: string;
9
+ }
10
+ /** Non-overlapping tracks; optimistic drag feedback emits input, one change per committed transaction. */
11
+ declare class DbVideoTimeline extends HTMLElement {
12
+ static get observedAttributes(): string[];
13
+ private _tracks;
14
+ private _clips;
15
+ private _markers;
16
+ private viewport?;
17
+ private canvas?;
18
+ private cursor?;
19
+ private hint?;
20
+ private drag?;
21
+ private raf;
22
+ private unsubscribe?;
23
+ private resize?;
24
+ private resizeDrag?;
25
+ private paneGrip?;
26
+ get rowHeight(): number;
27
+ set rowHeight(n: number);
28
+ get height(): number;
29
+ set height(n: number);
30
+ get minClipDuration(): number;
31
+ set minClipDuration(n: number);
32
+ get shortcuts(): boolean;
33
+ set shortcuts(n: boolean);
34
+ private trackHeight;
35
+ private number;
36
+ get duration(): number;
37
+ set duration(n: number);
38
+ get fps(): number;
39
+ set fps(n: number);
40
+ get zoom(): number;
41
+ set zoom(n: number);
42
+ get currentTime(): number;
43
+ set currentTime(n: number);
44
+ get snap(): boolean;
45
+ set snap(n: boolean);
46
+ get disabled(): boolean;
47
+ set disabled(n: boolean);
48
+ get selectedClipId(): string;
49
+ set selectedClipId(id: string);
50
+ get tracks(): VideoTrack[];
51
+ set tracks(value: VideoTrack[]);
52
+ get clips(): VideoClip[];
53
+ set clips(value: VideoClip[]);
54
+ get markers(): TimelineMarker[];
55
+ set markers(value: TimelineMarker[]);
56
+ private get scale();
57
+ connectedCallback(): void;
58
+ disconnectedCallback(): void;
59
+ attributeChangedCallback(name: string): void;
60
+ private syncHint;
61
+ private syncCursor;
62
+ private seek;
63
+ scrollToTime(time: number): void;
64
+ setZoom(value: number): void;
65
+ private node;
66
+ private render;
67
+ private select;
68
+ private timeAt;
69
+ private down;
70
+ private move;
71
+ private snapped;
72
+ private updateDrag;
73
+ private autoScroll;
74
+ private finish;
75
+ private up;
76
+ private cancel;
77
+ private rollback;
78
+ private onActionClick;
79
+ splitSelected(): void;
80
+ private trackIndexAt;
81
+ private finishResize;
82
+ private doubleResize;
83
+ private changeZoom;
84
+ private wheel;
85
+ private key;
86
+ }
87
+
88
+ export { DbVideoTimeline, TimelineMarker, VideoClip, VideoTrack };
89
+ export type { TimelineEditDetail };
@@ -0,0 +1 @@
1
+ import"./db-button.js";import{t,subscribeDbLocale as e}from"../core/i18n.js";import{clamp as i,emitEditor as s,editorEditable as r,finite as a,frameTime as d,editorButton as n,editorTimecode as h}from"./media-editor-model.js";class DbVideoTimeline extends HTMLElement{constructor(){super(...arguments),this._tracks=[],this._clips=[],this._markers=[],this.raf=0,this.down=t=>{if(this.drag||this.resizeDrag)return;if(this.disabled||0!==t.button||!(t.target instanceof Element)||t.target.closest("db-button"))return;const e=t.target.closest("[data-track-resize]"),i=t.target.closest("[data-pane-resize]");if(e||i){t.preventDefault(),this.viewport?.focus({preventScroll:!0});const i=this._tracks.find(t=>t.id===e?.dataset.trackResize);return this.resizeDrag={id:t.pointerId,trackId:i?.id,startY:t.clientY,initial:i?this.trackHeight(i):this.viewport.getBoundingClientRect().height,beforeTracks:this.tracks,beforeHeight:this.height},void this.setPointerCapture(t.pointerId)}const s=t.target.closest("[data-marker]");if(s)return void this.seek(Number(s.dataset.marker));const r=t.target.closest("[data-clip]"),a=this._clips.find(t=>t.id===r?.dataset.clip);if(a){if(this.select(a.id),this._tracks.find(t=>t.id===a.trackId)?.locked)return;this.viewport?.focus({preventScroll:!0})}else if(!t.target.closest("[data-seek],[data-playhead]"))return;if("touch"===t.pointerType&&!a&&!t.target.closest(".db-vt__ruler,[data-playhead]"))return;t.preventDefault();const d=t.target.closest("[data-trim]")?.dataset.trim,n=t.target.closest("[data-fade]")?.dataset.fade;this.drag={kind:a?"in"===n?"fade-in":"out"===n?"fade-out":"start"===d?"trim-start":"end"===d?"trim-end":"move":"seek",id:t.pointerId,clipId:a?.id,originX:t.clientX,originY:t.clientY,scroll:this.viewport.scrollLeft,before:this.clips,x:t.clientX,y:t.clientY,changed:!1,shift:t.shiftKey,ripple:t.metaKey||t.ctrlKey,duplicate:a&&!d&&!n&&t.altKey?`clip-${crypto.randomUUID()}`:void 0},this.setPointerCapture(t.pointerId),a||this.seek(this.timeAt(t.clientX)),this.raf=requestAnimationFrame(this.autoScroll)},this.move=t=>{if(this.resizeDrag?.id===t.pointerId){const e=this.resizeDrag,r=Math.round(e.initial+t.clientY-e.startY);return void(e.trackId?(this._tracks=e.beforeTracks.map(t=>t.id===e.trackId?{...t,height:i(r,32,120)}:t),this.render(),s(this,"db-track-input",{tracks:this.tracks})):(this.height=i(r,120,720),s(this,"db-resize-input",{height:this.height})))}this.drag?.id===t.pointerId&&(this.drag.x=t.clientX,this.drag.y=t.clientY,this.drag.shift=t.shiftKey,this.drag.ripple=t.metaKey||t.ctrlKey,"move"===this.drag.kind&&t.altKey&&!this.drag.duplicate&&(this.drag.duplicate=`clip-${crypto.randomUUID()}`),this.updateDrag())},this.autoScroll=()=>{if(!this.drag||!this.viewport)return;const t=this.viewport.getBoundingClientRect(),e=this.drag.x,s=e>t.right-48?i((e-t.right+48)/3,0,18):e<t.left+144+32?-i((t.left+176-e)/3,0,18):0,r=this.viewport.scrollLeft;this.viewport.scrollLeft+=s,r!==this.viewport.scrollLeft&&this.updateDrag(),this.raf=requestAnimationFrame(this.autoScroll)},this.up=t=>{this.resizeDrag?.id!==t.pointerId?this.drag?.id===t.pointerId&&(this.drag.changed&&(this.drag.x=t.clientX,this.drag.y=t.clientY,this.updateDrag()),this.finish()):this.finishResize()},this.cancel=()=>{this.finish(!0),this.finishResize(!0)},this.onActionClick=t=>{const e=t.target,i=e.dataset.action;if(!this.disabled&&("scroll"===i&&this.viewport?.scrollBy({left:Math.max(160,this.viewport.clientWidth-144),behavior:matchMedia("(prefers-reduced-motion: reduce)").matches?"instant":"smooth"}),"lock"===i||"mute"===i)){const t=e.dataset.track;this._tracks=this._tracks.map(e=>e.id===t?{...e,["lock"===i?"locked":"muted"]:!e["lock"===i?"locked":"muted"]}:e),this.render(),s(this,"db-track-change",{tracks:this.tracks})}},this.doubleResize=t=>{if(this.disabled||!(t.target instanceof Element))return;const e=t.target.closest("[data-track-resize],[data-pane-resize]");e&&(e.dataset.trackResize?(this._tracks=this._tracks.map(t=>t.id===e.dataset.trackResize?{...t,height:this.rowHeight}:t),this.render(),s(this,"db-track-change",{tracks:this.tracks})):(this.height=0,s(this,"db-resize",{height:0})))},this.wheel=t=>{if(this.disabled||!this.shortcuts||this.drag||this.resizeDrag||!t.metaKey&&!t.ctrlKey)return;t.preventDefault(),t.stopPropagation();const e=Math.exp(-t.deltaY*(1===t.deltaMode?.04:.002));this.changeZoom(this.zoom*e,t.clientX-this.viewport.getBoundingClientRect().left,"wheel")},this.key=t=>{if(this.disabled||r(t.target)||t.isComposing)return;if("Escape"===t.key&&this.drag)return t.preventDefault(),void this.finish(!0);if("Escape"===t.key&&this.resizeDrag)return t.preventDefault(),void this.finishResize(!0);const e=t.target.closest("[data-track-resize],[data-pane-resize]");if(e&&["ArrowUp","ArrowDown","Home"].includes(t.key)){t.preventDefault();const r="ArrowUp"===t.key?-1:1;return void(e.dataset.trackResize?(this._tracks=this._tracks.map(s=>s.id===e.dataset.trackResize?{...s,height:"Home"===t.key?this.rowHeight:i(this.trackHeight(s)+r*(t.shiftKey?8:2),32,120)}:s),this.render(),s(this,"db-track-change",{tracks:this.tracks}),this.querySelector(`[data-track-resize="${CSS.escape(e.dataset.trackResize)}"]`)?.focus()):(this.height="Home"===t.key?0:i((this.height||this.viewport.clientHeight)+r*(t.shiftKey?24:8),120,720),s(this,"db-resize",{height:this.height}),this.paneGrip?.focus()))}if(this.shortcuts&&!t.altKey&&["+","=","-","0"].includes(t.key))return t.preventDefault(),t.stopPropagation(),void("0"===t.key?this.changeZoom((this.viewport.clientWidth-144)/this.duration/.8,void 0,"fit"):this.changeZoom(this.zoom*("-"===t.key?1/1.2:1.2)));if(this.disabled||r(t.target)||t.ctrlKey||t.metaKey||t.altKey)return;const a=t.target.closest("[data-trim],[data-fade]");if(a&&["ArrowLeft","ArrowRight"].includes(t.key)){const e=a.closest("[data-clip]"),i=this._clips.find(t=>t.id===e?.dataset.clip);if(!i||this._tracks.find(t=>t.id===i.trackId)?.locked)return;t.preventDefault();const s="start"===a.dataset.trim?"trim-start":"end"===a.dataset.trim?"trim-end":"in"===a.dataset.fade?"fade-in":"fade-out";this.drag={kind:s,id:-1,clipId:i.id,originX:0,originY:0,scroll:this.viewport.scrollLeft,before:this.clips,x:("ArrowLeft"===t.key?-1:1)*(t.shiftKey?10:1)/this.fps*this.scale,y:0,changed:!1,shift:!0,ripple:t.metaKey||t.ctrlKey},this.updateDrag(),this.finish();const r=a.dataset.focus;return void Array.from(this.querySelectorAll("[data-focus]")).find(t=>t.dataset.focus===r)?.focus({preventScroll:!0})}if(["ArrowLeft","ArrowRight","Home","End"].includes(t.key))t.preventDefault(),this.seek("Home"===t.key?0:"End"===t.key?this.duration:this.currentTime+("ArrowLeft"===t.key?-1:1)*(t.shiftKey?10:1)/this.fps),this.scrollToTime(this.currentTime);else if("s"===t.key.toLowerCase())t.preventDefault(),this.splitSelected();else if("Delete"===t.key||"Backspace"===t.key){const e=this._clips.find(t=>t.id===this.selectedClipId);if(!e||this._tracks.find(t=>t.id===e.trackId)?.locked)return;t.preventDefault(),this._clips=this._clips.filter(t=>t.id!==e.id),this.select(""),this.render(),s(this,"db-change",{clips:this.clips,clipId:e.id,action:"delete"})}else"Enter"===t.key&&t.target instanceof HTMLElement&&t.target.dataset.clip&&(t.preventDefault(),this.select(t.target.dataset.clip))}}static get observedAttributes(){return["duration","fps","current-time","zoom","snap","disabled","selected-clip-id","row-height","height","min-clip-duration","shortcuts"]}get rowHeight(){return i(this.number("row-height",42),32,120)}set rowHeight(t){this.setAttribute("row-height",String(t))}get height(){const t=this.number("height",0);return t>0?i(t,120,720):0}set height(t){t>0?this.setAttribute("height",String(t)):this.removeAttribute("height")}get minClipDuration(){return Math.min(this.duration,Math.max(1/this.fps,Math.ceil(this.number("min-clip-duration",.5)*this.fps)/this.fps))}set minClipDuration(t){this.setAttribute("min-clip-duration",String(t))}get shortcuts(){return"false"!==this.getAttribute("shortcuts")}set shortcuts(t){this.setAttribute("shortcuts",String(t))}trackHeight(t){return i(a(t.height,this.rowHeight),32,120)}number(t,e){const i=this.getAttribute(t);return i?.trim()&&Number.isFinite(Number(i))?Number(i):e}get duration(){return i(this.number("duration",30),1/this.fps,86400)}set duration(t){this.setAttribute("duration",String(t))}get fps(){return i(Math.round(this.number("fps",30)),1,120)}set fps(t){this.setAttribute("fps",String(t))}get zoom(){return i(this.number("zoom",100),10,800)}set zoom(t){this.setAttribute("zoom",String(t))}get currentTime(){return d(i(this.number("current-time",0),0,this.duration),this.fps)}set currentTime(t){this.setAttribute("current-time",String(t))}get snap(){return"false"!==this.getAttribute("snap")}set snap(t){this.setAttribute("snap",String(t))}get disabled(){return this.hasAttribute("disabled")}set disabled(t){this.toggleAttribute("disabled",t)}get selectedClipId(){return this.getAttribute("selected-clip-id")||""}set selectedClipId(t){this.setAttribute("selected-clip-id",t)}get tracks(){return this._tracks.map(t=>({...t}))}set tracks(t){const e=new Set;this._tracks=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&!e.has(t.id)&&!!e.add(t.id)).map(t=>({...t,label:String(t.label||t.id),kind:["video","audio","title","mixed"].includes(t.kind)?t.kind:"video"})),this.render()}get clips(){return this._clips.map(t=>({...t,...t.effects?{effects:t.effects.map(t=>({...t}))}:{}}))}set clips(t){if(this.drag)return;const e=new Set;this._clips=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&!e.has(t.id)&&!!e.add(t.id)).map(t=>{const e=d(i(a(t.start,0),0,this.duration-1/this.fps),this.fps),s=Math.min(this.duration-e,Math.max(1/this.fps,d(a(t.duration,1),this.fps))),r=d(i(a(t.fadeIn,0),0,s),this.fps),n=d(i(a(t.fadeOut,0),0,Math.max(0,s-r)),this.fps);return{...t,label:String(t.label||t.id),start:e,duration:s,sourceOffset:Math.max(0,a(t.sourceOffset,0)),...void 0!==t.fadeIn?{fadeIn:r}:{},...void 0!==t.fadeOut?{fadeOut:n}:{},...t.effects?{effects:Array.isArray(t.effects)?t.effects.filter(t=>t&&"string"==typeof t.id).map(t=>({...t,label:String(t.label||t.id)})):[]}:{}}}),this.render()}get markers(){return this._markers.map(t=>({...t}))}set markers(t){this._markers=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&Number.isFinite(t.time)).map(t=>({...t,label:String(t.label),time:i(t.time,0,this.duration)})),this.render()}get scale(){return.8*this.zoom}connectedCallback(){if(this.classList.add("designbase-wc-video-timeline"),this.setAttribute("role","region"),!this.viewport){this.viewport=document.createElement("div"),this.viewport.className="db-vt__viewport",this.viewport.tabIndex=0,this.canvas=document.createElement("div"),this.canvas.className="db-vt__canvas",this.viewport.append(this.canvas),this.hint=document.createElement("div"),this.hint.className="db-vt__hint";const e=n(t("media.more"),"chevron-right","scroll");this.hint.append(e),this.paneGrip=document.createElement("div"),this.paneGrip.className="db-vt__pane-resize",this.paneGrip.dataset.paneResize="",this.paneGrip.tabIndex=0,this.paneGrip.setAttribute("role","separator"),this.paneGrip.setAttribute("aria-orientation","horizontal"),this.append(this.viewport,this.hint,this.paneGrip),this.viewport.addEventListener("scroll",()=>this.syncHint())}this.viewport.addEventListener("wheel",this.wheel,{passive:!1}),this.addEventListener("dblclick",this.doubleResize),this.addEventListener("pointerdown",this.down),this.addEventListener("pointermove",this.move),this.addEventListener("pointerup",this.up),this.addEventListener("pointercancel",this.cancel),this.addEventListener("lostpointercapture",this.cancel),this.addEventListener("keydown",this.key),this.addEventListener("db-click",this.onActionClick),this.unsubscribe=e(()=>this.render()),this.resize=new ResizeObserver(()=>this.syncHint()),this.resize.observe(this),this.render()}disconnectedCallback(){this.rollback(),this.resize?.disconnect(),this.unsubscribe?.(),this.viewport?.removeEventListener("wheel",this.wheel),this.removeEventListener("dblclick",this.doubleResize),this.finishResize(!0),this.removeEventListener("pointerdown",this.down),this.removeEventListener("pointermove",this.move),this.removeEventListener("pointerup",this.up),this.removeEventListener("pointercancel",this.cancel),this.removeEventListener("lostpointercapture",this.cancel),this.removeEventListener("keydown",this.key),this.removeEventListener("db-click",this.onActionClick)}attributeChangedCallback(t){"disabled"===t&&this.disabled&&(this.finish(!0),this.finishResize(!0)),"current-time"===t?this.syncCursor():this.render()}syncHint(){if(this.syncCursor(),this.viewport)for(const t of this.querySelectorAll("[data-marker]"))t.style.visibility=Number(t.dataset.marker)*this.scale<this.viewport.scrollLeft?"hidden":"";this.viewport&&this.hint&&(this.hint.hidden=this.viewport.scrollLeft+this.viewport.clientWidth>=this.viewport.scrollWidth-2)}syncCursor(){this.cursor&&(this.cursor.style.left=144+this.currentTime*this.scale+"px",this.cursor.style.visibility=this.currentTime*this.scale<(this.viewport?.scrollLeft||0)?"hidden":"",this.cursor.setAttribute("aria-valuenow",String(this.currentTime)),this.cursor.setAttribute("aria-valuetext",h(this.currentTime,this.fps)))}seek(t){this.currentTime=d(i(t,0,this.duration),this.fps),s(this,"db-seek",{time:this.currentTime})}scrollToTime(t){this.viewport&&(this.viewport.scrollLeft=Math.max(0,i(t,0,this.duration)*this.scale-(this.viewport.clientWidth-144)/2))}setZoom(t){const e=this.currentTime;this.zoom=t,this.scrollToTime(e)}node(t,e,i){const s=document.createElement(t);return s.className=e,void 0!==i&&(s.textContent=i),s}render(){if(!this.canvas||!this.viewport)return;this.querySelector(":scope > .db-vt__readout")?.remove(),this.viewport.style.height=this.height?`${this.height}px`:"",this.viewport.style.maxHeight=this.height?"720px":"360px",this.paneGrip&&(this.paneGrip.setAttribute("aria-label",t("media.resizeTimeline")),this.paneGrip.setAttribute("title",t("media.resizeTimelineHint")),this.paneGrip.setAttribute("aria-valuemin","120"),this.paneGrip.setAttribute("aria-valuemax","720"),this.paneGrip.setAttribute("aria-valuenow",String(this.height||this.viewport.clientHeight)));const e=this.hint?.querySelector("db-button");e?.setAttribute("aria-label",t("media.more")),e?.setAttribute("title",t("media.more"));const s=this.ownerDocument.activeElement?.dataset.focus,r=document.createDocumentFragment(),a=this.node("div","db-vt__ruler"),d=this.node("span","db-vt__corner",t("media.tracks"));a.append(d);const o=[1/this.fps,.1,.25,.5,1,2,5,10,30,60,300,600,3600].find(t=>t*this.scale>=64&&this.duration/t<=200)||3600;for(let t=0;t<=this.duration;t+=o){const e=this.node("span","db-vt__tick",h(t,this.fps));e.style.left=144+t*this.scale+"px",a.append(e)}a.dataset.seek="",r.append(a),this._tracks.forEach((e,s)=>{const a=this.node("div","db-vt__row");a.dataset.track=e.id,a.dataset.kind=e.kind,a.style.setProperty("--db-vt-track-height",`${this.trackHeight(e)}px`),a.dataset.condensed=String(this.trackHeight(e)<44),a.dataset.locked=String(!!e.locked),a.dataset.muted=String(!!e.muted);const d=this.node("div","db-vt__track");d.append(this.node("span","db-vt__track-name",e.label));for(const i of["lock","mute"]){const s=t("lock"===i?e.locked?"media.trackLocked":"media.trackUnlocked":e.muted?"media.soundOff":"media.soundOn"),r=n(`${s} · ${e.label}`,"lock"===i?e.locked?"lock":"unlock":e.muted?"mute":"volume-up",i);r.setAttribute("variant","ghost"),r.dataset.track=e.id,r.setAttribute("aria-pressed",String("lock"===i?!!e.locked:!!e.muted)),r.toggleAttribute("disabled",this.disabled),d.append(r)}a.append(d);const c=this.node("div","db-vt__lane");c.dataset.seek="",c.style.width=this.duration*this.scale+"px",c.style.setProperty("--tick",o*this.scale+"px"),this._clips.filter(t=>t.trackId===e.id).forEach(s=>{const r=this.node("div","db-vt__clip");r.dataset.clip=s.id,r.dataset.focus=s.id,r.dataset.selected=String(this.selectedClipId===s.id),r.setAttribute("role","group"),r.tabIndex=0,r.setAttribute("aria-label",`${s.label}, ${h(s.start,this.fps)}, ${t("media.duration")} ${h(s.duration,this.fps)}`),r.setAttribute("aria-disabled",String(this.disabled||!!e.locked));const a=this.selectedClipId===s.id;r.dataset.appearance=s.appearance||("audio"===(s.kind||e.kind)?"waveform":"standard"),r.dataset.narrow=String(s.duration*this.scale<100),r.dataset.editing=String(this.drag?.clipId===s.id),r.style.left=s.start*this.scale+"px",r.style.width=`${Math.max(24,s.duration*this.scale)}px`,r.dataset.hitProxy=String(s.duration*this.scale<24),r.style.setProperty("--db-vt-actual-width",s.duration*this.scale+"px");for(const i of["start","end"]){const d=this.node("span","db-vt__trim");d.dataset.trim=i,d.dataset.focus=`${s.id}-trim-${i}`,d.tabIndex=!a||this.disabled||e.locked?-1:0,d.setAttribute("role","slider"),d.setAttribute("aria-label",`${t("start"===i?"media.trimStart":"media.trimEnd")} · ${s.label}`),d.setAttribute("aria-valuemin","0"),d.setAttribute("aria-valuemax",String(this.duration)),d.setAttribute("aria-valuenow",String("start"===i?s.start:s.start+s.duration)),d.setAttribute("aria-valuetext",h("start"===i?s.start:s.start+s.duration,this.fps)),d.setAttribute("title",t("start"===i?"media.trimStart":"media.trimEnd")),d.append(this.node("i","db-vt__grip")),r.append(d)}if("filmstrip"===s.appearance&&s.thumbnail){const t=this.node("div","db-vt__filmstrip");t.setAttribute("aria-hidden","true");for(let e=0;e<Math.min(24,Math.ceil(s.duration*this.scale/64));e++){const e=document.createElement("img");e.src=s.thumbnail,e.alt="",e.draggable=!1,t.append(e)}r.append(t)}for(const d of["in","out"]){const n=i("in"===d?s.fadeIn||0:s.fadeOut||0,0,s.duration),h=this.node("span","db-vt__fade");if(h.dataset.fadeArea=d,h.style.width=n*this.scale+"px",h.setAttribute("aria-hidden","true"),r.append(h),a){const i=this.node("span","db-vt__fade-handle");i.dataset.fade=d,i.dataset.focus=`${s.id}-fade-${d}`,i.style["in"===d?"left":"right"]=n*this.scale+"px",i.tabIndex=this.disabled||e.locked?-1:0,i.setAttribute("role","slider"),i.setAttribute("aria-label",`${t("in"===d?"media.fadeIn":"media.fadeOut")} · ${s.label}`),i.setAttribute("aria-valuemin","0"),i.setAttribute("aria-valuemax",String(s.duration)),i.setAttribute("aria-valuenow",String(n)),i.setAttribute("title",t("in"===d?"media.fadeIn":"media.fadeOut")),r.append(i)}}if(r.append(this.node("span","db-vt__clip-label",s.label)),"audio"===(s.kind||e.kind)||"waveform"===s.appearance){const t=this.node("span","db-vt__wave");t.setAttribute("aria-hidden","true");for(let e=0;e<Math.min(160,Math.ceil(s.duration*this.scale/7));e++){const i=this.node("i","");i.style.height=20+75*Math.abs(Math.sin(1.7*e)*Math.cos(.4*e))+"%",t.append(i)}r.append(t)}if(s.effects?.length){const e=this.node("span","db-vt__effects");e.setAttribute("aria-label",t("media.effects"));for(const t of s.effects){const i=this.node("span","db-vt__effect",t.label);i.dataset.enabled=String(!1!==t.enabled),i.dataset.kind=t.kind,i.setAttribute("title",t.label),e.append(i)}r.append(e)}c.append(r)}),a.append(c);const l=this.node("div","db-vt__track-resize");l.dataset.trackResize=e.id,l.tabIndex=0,l.setAttribute("role","separator"),l.setAttribute("aria-orientation","horizontal"),l.setAttribute("aria-label",`${t("media.resizeTrack")} · ${e.label}`),l.setAttribute("aria-valuemin","32"),l.setAttribute("aria-valuemax","120"),l.setAttribute("aria-valuenow",String(this.trackHeight(e))),a.append(l),r.append(a)});for(const e of this._markers){const i=this.node("button","db-vt__marker");i.type="button",i.dataset.marker=String(e.time),i.style.left=144+e.time*this.scale+"px";const s=`${t("media.marker")} · ${e.label} · ${h(e.time,this.fps)}`;i.setAttribute("aria-label",s),i.setAttribute("title",s),r.append(i)}if(this.cursor=this.node("div","db-vt__playhead"),this.cursor.dataset.playhead="",this.cursor.tabIndex=0,this.cursor.setAttribute("role","slider"),this.cursor.dataset.focus="playhead",this.cursor.setAttribute("aria-label",t("media.playhead")),this.cursor.setAttribute("aria-valuemin","0"),this.cursor.setAttribute("aria-valuemax",String(this.duration)),this.cursor.setAttribute("aria-disabled",String(this.disabled)),r.append(this.cursor),this._tracks.length||r.append(this.node("p","db-vt__empty",t("media.emptyTracks"))),this.canvas.style.width=144+this.duration*this.scale+"px",this.drag?.clipId){const e=this._clips.find(t=>t.id===this.drag?.clipId);if(e){const i="fade-in"===this.drag.kind?e.fadeIn||0:"fade-out"===this.drag.kind?e.fadeOut||0:"trim-end"===this.drag.kind?e.start+e.duration:e.start,s=this.node("div","db-vt__readout",`${t("media."+("trim-start"===this.drag.kind?"trimStart":"trim-end"===this.drag.kind?"trimEnd":"fade-in"===this.drag.kind?"fadeIn":"fade-out"===this.drag.kind?"fadeOut":"time"))} ${h(i,this.fps)} / ${t("media.duration")} ${h(e.duration,this.fps)}`);this.append(s)}}this.canvas.replaceChildren(r),this.viewport.setAttribute("aria-label",t("media.timeline")),this.syncCursor(),this.syncHint(),s&&!this.drag&&Array.from(this.querySelectorAll("[data-focus]")).find(t=>t.dataset.focus===s)?.focus({preventScroll:!0})}select(t){this.selectedClipId!==t&&(this.selectedClipId=t,s(this,"db-selection-change",{clipId:t}))}timeAt(t){return(t-this.viewport.getBoundingClientRect().left+this.viewport.scrollLeft-144)/this.scale}snapped(t,e){let i=d(t,this.fps);if(this.snap&&!this.drag?.shift){const t=[0,this.currentTime,...this._markers.map(t=>t.time),...this._clips.filter(t=>t.id!==e).flatMap(t=>[t.start,t.start+t.duration])].reduce((t,e)=>Math.abs(e-i)<Math.abs(t-i)?e:t,1/0);Math.abs(t-i)*this.scale<=8&&(i=d(t,this.fps))}return i}updateDrag(){const t=this.drag;if(!t)return;if("seek"===t.kind)return void this.seek(this.timeAt(t.x));const e=t.before.find(e=>e.id===t.clipId);if(!e)return;const r=(t.x-t.originX+this.viewport.scrollLeft-t.scroll)/this.scale;let a=e.start,n=e.duration,h=e.sourceOffset||0,o=e.trackId;if("fade-in"===t.kind||"fade-out"===t.kind){const a="fade-in"===t.kind?"fadeIn":"fadeOut",n="fade-in"===t.kind?e.fadeOut||0:e.fadeIn||0,h=d(i((e[a]||0)+("fade-in"===t.kind?r:-r),0,Math.max(0,e.duration-n)),this.fps);return this._clips=t.before.map(t=>t.id===e.id?{...t,[a]:h}:t),t.changed=JSON.stringify(this._clips)!==JSON.stringify(t.before),this.render(),void s(this,"db-input",{clips:this.clips,clipId:e.id,action:t.kind})}if("move"===t.kind){a=this.snapped(a+r,e.id);const s=this.canvas.getBoundingClientRect(),d=this.trackIndexAt(t.y-s.top-36),h=this._tracks[i(d,0,this._tracks.length-1)],c=this._tracks.find(t=>t.id===e.trackId);!h||h.locked||"mixed"!==h.kind&&h.kind!==(e.kind||c?.kind)||(o=h.id),a=i(a,0,this.duration-n)}else if("trim-start"===t.kind)a=i(this.snapped(a+r,e.id),Math.max(0,e.start-h),e.start+n-Math.min(this.minClipDuration,e.start+n-Math.max(0,e.start-h))),n=e.start+e.duration-a,h+=a-e.start;else{const t=e.sourceDuration?Math.min(this.duration-a,e.sourceDuration-h):this.duration-a;n=i(this.snapped(a+n+r,e.id)-a,Math.min(this.minClipDuration,t),t)}const c=t.ripple&&("trim-start"===t.kind||"trim-end"===t.kind),l=t.before.filter(t=>t.id!==e.id&&t.trackId===e.trackId&&t.start>=e.start+e.duration-1e-6);let p=0;if(c){"trim-start"===t.kind&&(a=e.start);const i=Math.max(e.start+e.duration,...l.map(t=>t.start+t.duration));n=Math.min(n,e.duration+this.duration-i),p=d(n-e.duration,this.fps)}let u=t.before.map(t=>l.some(e=>e.id===t.id)&&c?{...t,start:d(t.start+p,this.fps)}:t);if("move"===t.kind&&t.ripple){const i=t.before.filter(i=>i.trackId===o&&(t.duplicate||i.id!==e.id)).sort((t,e)=>t.start-e.start),s=i.find(t=>a>t.start+1e-6&&a<t.start+t.duration-1e-6);s&&(a=s.start);let r=a+n;const h=new Map;for(const t of i){if(t.start<a-1e-6)continue;const e=Math.max(t.start,r);h.set(t.id,e),r=e+t.duration}if(r>this.duration+1e-6)return;u=t.before.map(t=>h.has(t.id)?{...t,start:d(h.get(t.id),this.fps)}:t)}if(u.some(i=>(t.duplicate||i.id!==e.id)&&i.trackId===o&&a<i.start+i.duration-1e-6&&a+n>i.start+1e-6))return;const f=t.duplicate||e.id;t.duplicate&&(u=[...u,{...e,id:f}]),this._clips=u.map(t=>t.id===f?{...t,kind:t.kind||("mixed"===this._tracks.find(t=>t.id===e.trackId)?.kind?"video":this._tracks.find(t=>t.id===e.trackId)?.kind),start:d(a,this.fps),duration:d(n,this.fps),sourceOffset:d(h,this.fps),trackId:o,...void 0!==t.fadeIn?{fadeIn:Math.min(t.fadeIn,n)}:{},...void 0!==t.fadeOut?{fadeOut:Math.min(t.fadeOut,Math.max(0,n-(t.fadeIn||0)))}:{}}:t),t.changed=JSON.stringify(this._clips)!==JSON.stringify(t.before),this.render(),s(this,"db-input",{clips:this.clips,clipId:e.id,action:t.kind})}finish(t=!1){const e=this.drag;e&&(cancelAnimationFrame(this.raf),this.drag=void 0,this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id),t?(this._clips=e.before,this.render(),s(this,"db-input",{clips:this.clips,clipId:e.clipId,action:e.kind,cancelled:!0})):e.changed&&e.clipId&&"seek"!==e.kind&&s(this,"db-change",{clips:this.clips,clipId:e.clipId,action:e.kind}),this.render(),this.viewport?.focus({preventScroll:!0}))}rollback(){this.finish(!0)}splitSelected(){const t=this._clips.find(t=>t.id===this.selectedClipId);if(this.disabled||!t||this._tracks.find(e=>e.id===t.trackId)?.locked||t.duration<2*this.minClipDuration||this.currentTime<t.start+this.minClipDuration||this.currentTime>t.start+t.duration-this.minClipDuration)return;const e=this.currentTime;let i=t.id+"-split";for(;this._clips.some(t=>t.id===i);)i+="-1";this._clips=this._clips.flatMap(s=>s.id===t.id?[{...s,duration:e-s.start,fadeOut:0,fadeIn:Math.min(s.fadeIn||0,e-s.start)},{...s,id:i,fadeIn:0,fadeOut:Math.min(s.fadeOut||0,s.start+s.duration-e),start:e,duration:s.start+s.duration-e,sourceOffset:(s.sourceOffset||0)+e-s.start}]:[s]),this.render(),s(this,"db-change",{clips:this.clips,action:"split",clipId:t.id})}trackIndexAt(t){let e=0;for(let i=0;i<this._tracks.length;i++)if(e+=this.trackHeight(this._tracks[i]),t<e)return i;return this._tracks.length-1}finishResize(t=!1){const e=this.resizeDrag;e&&(this.resizeDrag=void 0,this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id),t?(this._tracks=e.beforeTracks,this.height=e.beforeHeight,this.render(),s(this,e.trackId?"db-track-input":"db-resize-input",e.trackId?{tracks:this.tracks,cancelled:!0}:{height:this.height,cancelled:!0})):s(this,e.trackId?"db-track-change":"db-resize",e.trackId?{tracks:this.tracks}:{height:this.height}))}changeZoom(t,e,r="keyboard"){if(!this.viewport)return;const a=this.zoom,d=e??Math.min(this.viewport.clientWidth-8,Math.max(152,144+this.currentTime*this.scale-this.viewport.scrollLeft)),n=(this.viewport.scrollLeft+d-144)/this.scale;this.zoom=Math.round(100*i(t,10,800))/100,this.viewport.scrollLeft="fit"===r?0:Math.max(0,144+n*this.scale-d),this.zoom!==a&&s(this,"db-zoom-change",{value:this.zoom,previousValue:a,anchorTime:n,source:r})}}customElements.get("db-video-timeline")||customElements.define("db-video-timeline",DbVideoTimeline);export{DbVideoTimeline};
@@ -0,0 +1,14 @@
1
+ type EasingCurve = [number, number, number, number];
2
+ interface EasingPreset {
3
+ id: string;
4
+ curve: EasingCurve;
5
+ group: 'basic' | 'expressive';
6
+ }
7
+ declare const EASING_PRESETS: readonly EasingPreset[];
8
+ declare function evaluateEasing(value: EasingCurve, time: number): number;
9
+ /** Normalized velocity (progress / normalized time). 1 = constant speed; multiply by value delta / duration for real units/s. */
10
+ declare function evaluateEasingVelocity(value: EasingCurve, time: number): number;
11
+ declare function easingCss(value: EasingCurve): string;
12
+
13
+ export { EASING_PRESETS, easingCss, evaluateEasing, evaluateEasingVelocity };
14
+ export type { EasingCurve, EasingPreset };
@@ -0,0 +1 @@
1
+ import{clamp as e,finite as r}from"./media-editor-model.js";const i=[{id:"linear",curve:[0,0,1,1],group:"basic"},{id:"ease",curve:[.25,.1,.25,1],group:"basic"},{id:"ease-in",curve:[.42,0,1,1],group:"basic"},{id:"ease-out",curve:[0,0,.58,1],group:"basic"},{id:"ease-in-out",curve:[.42,0,.58,1],group:"basic"},{id:"smooth",curve:[.45,0,.55,1],group:"expressive"},{id:"snappy",curve:[.76,0,.24,1],group:"expressive"},{id:"swift",curve:[.16,1,.3,1],group:"expressive"},{id:"gentle",curve:[.37,0,.63,1],group:"expressive"},{id:"emphasized",curve:[.2,0,0,1],group:"expressive"}];function normalizeEasing(i){return Array.isArray(i)&&4===i.length?i.map((i,a)=>Math.round(1e4*e(r(i,a<2?0:1),0,1))/1e4):[.42,0,.58,1]}const cubic=(e,r,i)=>3*(1-e)**2*e*r+3*(1-e)*e*e*i+e**3,derivative=(e,r,i)=>3*(1-e)**2*r+6*(1-e)*e*(i-r)+3*e*e*(1-i);function parameter(e,r){let i=0,a=1;for(let s=0;s<28;s++){const s=(i+a)/2;cubic(s,e[0],e[2])<r?i=s:a=s}return(i+a)/2}function evaluateEasing(r,i){const a=normalizeEasing(r),s=e(i,0,1);return 0===s||1===s?s:cubic(parameter(a,s),a[1],a[3])}function evaluateEasingVelocity(r,i){const a=normalizeEasing(r),s=parameter(a,e(i,1e-5,.99999)),n=derivative(s,a[0],a[2]);return derivative(s,a[1],a[3])/Math.max(1e-6,n)}function easingCss(e){return`cubic-bezier(${normalizeEasing(e).join(", ")})`}export{i as EASING_PRESETS,easingCss,evaluateEasing,evaluateEasingVelocity,normalizeEasing};
@@ -0,0 +1,60 @@
1
+ import './db-button.js';
2
+
3
+ /** Time is seconds; editing is quantized to integral frames at the specified FPS. */
4
+ interface VideoTrack {
5
+ id: string;
6
+ label: string;
7
+ kind: 'video' | 'audio' | 'title' | 'mixed';
8
+ locked?: boolean;
9
+ muted?: boolean;
10
+ height?: number;
11
+ }
12
+ interface VideoClipEffect {
13
+ id: string;
14
+ label: string;
15
+ kind: 'color' | 'blur' | 'transform' | 'opacity' | 'speed';
16
+ enabled?: boolean;
17
+ }
18
+ interface VideoClip {
19
+ kind?: 'video' | 'audio' | 'title';
20
+ id: string;
21
+ trackId: string;
22
+ label: string;
23
+ start: number;
24
+ duration: number;
25
+ sourceOffset?: number;
26
+ sourceDuration?: number;
27
+ fadeIn?: number;
28
+ fadeOut?: number;
29
+ effects?: VideoClipEffect[];
30
+ appearance?: 'standard' | 'filmstrip' | 'waveform' | 'compact';
31
+ thumbnail?: string;
32
+ }
33
+ interface TimelineMarker {
34
+ id: string;
35
+ time: number;
36
+ label: string;
37
+ }
38
+ type KeyframeEasing = 'linear' | 'hold' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bezier';
39
+ interface KeyframeTangent {
40
+ time: number;
41
+ value: number;
42
+ }
43
+ interface EditorKeyframe {
44
+ id: string;
45
+ time: number;
46
+ value: number;
47
+ easing?: KeyframeEasing;
48
+ bezier?: [number, number, number, number];
49
+ inTangent?: KeyframeTangent;
50
+ outTangent?: KeyframeTangent;
51
+ tangentMode?: 'aligned' | 'free';
52
+ }
53
+ declare function frameTime(time: number, fps?: number): number;
54
+ declare function editorTimecode(time: number, fps?: number): string;
55
+ declare function evaluateKeyframes(keys: EditorKeyframe[], time: number): number;
56
+ declare function keyframeEasingCurve(keys: EditorKeyframe[], id: string): [number, number, number, number] | undefined;
57
+ declare function applyKeyframeEasing(keys: EditorKeyframe[], id: string, easing: KeyframeEasing, bezier?: [number, number, number, number]): EditorKeyframe[];
58
+
59
+ export { applyKeyframeEasing, editorTimecode, evaluateKeyframes, frameTime, keyframeEasingCurve };
60
+ export type { EditorKeyframe, KeyframeEasing, KeyframeTangent, TimelineMarker, VideoClip, VideoClipEffect, VideoTrack };
@@ -0,0 +1 @@
1
+ const clamp=(e,t,n)=>Math.min(n,Math.max(t,e)),finite=(e,t)=>"number"==typeof e&&Number.isFinite(e)?e:t;function frameTime(e,t=30){return Math.round(e*t)/t}function editorTimecode(e,t=30){const n=Math.max(1,Math.round(t)),i=Math.max(0,Math.round(e*n));return[Math.floor(i/(3600*n)),Math.floor(i/(60*n))%60,Math.floor(i/n)%60,i%n].map(e=>String(e).padStart(2,"0")).join(":")}function easingPoints(e){return"bezier"===e.easing?e.bezier||[.25,.1,.25,1]:"ease-in"===e.easing?[.42,0,1,1]:"ease-out"===e.easing?[0,0,.58,1]:"ease-in-out"===e.easing?[.42,0,.58,1]:[0,0,1,1]}const cubic=(e,t,n)=>3*(1-e)**2*e*t+3*(1-e)*e*e*n+e**3;function evaluateKeyframes(e,t){const n=[...e].sort((e,t)=>e.time-t.time);if(!n.length)return 0;if(t<=n[0].time)return n[0].value;const i=n.findIndex(e=>e.time>t);if(i<0)return n[n.length-1].value;const a=n[i-1],u=n[i];if("hold"===a.easing)return a.value;const o=keyframeSegment(n,i-1),r=u.time-a.time,m=clamp((t-a.time)/r,0,1);let s=0,l=1;for(let e=0;e<28;e++){const e=(s+l)/2;cubic(e,(o.time1-a.time)/r,(o.time2-a.time)/r)<m?s=e:l=e}const g=(s+l)/2,v=1-g;return v*v*v*a.value+3*v*v*g*o.value1+3*v*g*g*o.value2+g*g*g*u.value}function keyframeTangents(e,t){const n=e[t],i=e[t-1],a=e[t+1];if(!n)return{};const u="aligned"===n.tangentMode?Math.min(i?n.time-i.time:1/0,a?a.time-n.time:1/0)/2:1/0;let o,r;if(i){const e=n.time-i.time,t=n.value-i.value,a=easingPoints(i);o=n.inTangent?{time:-Math.min(Math.abs(n.inTangent.time),e/2,u),value:n.inTangent.value}:{time:(a[2]-1)*e,value:(a[3]-1)*t}}if(a){const e=a.time-n.time,t=a.value-n.value,i=easingPoints(n);r=n.outTangent?{time:Math.min(Math.abs(n.outTangent.time),e/2,u),value:n.outTangent.value}:{time:i[0]*e,value:i[1]*t}}return{incoming:o,outgoing:r}}function keyframeSegment(e,t){const n=e[t],i=e[t+1],a=i.time-n.time,u=i.value-n.value,o=easingPoints(n),r=keyframeTangents(e,t).outgoing,m=keyframeTangents(e,t+1).incoming;return{time1:n.outTangent?n.time+r.time:n.time+o[0]*a,value1:n.outTangent?n.value+r.value:n.value+o[1]*u,time2:i.inTangent?i.time+m.time:n.time+o[2]*a,value2:i.inTangent?i.value+m.value:n.value+o[3]*u}}function keyframeEasingCurve(e,t){const n=e.findIndex(e=>e.id===t),i=e[n],a=e[n+1];if(!i||!a)return;const u=keyframeSegment(e,n),o=a.time-i.time,r=a.value-i.value;return Math.abs(r)<1e-9?Math.abs(u.value1-i.value)<1e-9&&Math.abs(u.value2-i.value)<1e-9?[0,0,1,1]:void 0:[(u.time1-i.time)/o,(u.value1-i.value)/r,(u.time2-i.time)/o,(u.value2-i.value)/r]}function applyKeyframeEasing(e,t,n,i){const a=e.findIndex(e=>e.id===t);return e.map((e,t)=>t===a?{...e,easing:n,bezier:i,outTangent:void 0,tangentMode:e.inTangent?"free":void 0}:t===a+1?{...e,inTangent:void 0,tangentMode:e.outTangent?"free":void 0}:e)}function editorEditable(e){return e instanceof Element&&!!e.closest("input,textarea,select,[contenteditable=true],db-input,db-select,db-textarea")}function editorButton(e,t,n){const i=document.createElement("db-button");return i.setAttribute("size","s"),i.setAttribute("variant","tertiary"),i.setAttribute("icon-only",""),i.setAttribute("start-icon",t),i.setAttribute("aria-label",e),i.setAttribute("title",e),i.dataset.action=n,i}function emitEditor(e,t,n){e.dispatchEvent(new CustomEvent(t,{detail:n,bubbles:!0,composed:!0}))}export{applyKeyframeEasing,clamp,easingPoints,editorButton,editorEditable,editorTimecode,emitEditor,evaluateKeyframes,finite,frameTime,keyframeEasingCurve,keyframeSegment,keyframeTangents};
@@ -1 +1 @@
1
- const e={"selectionBar.count":"{count} selected","selectionBar.clear":"Clear","selectionBar.label":"Selection actions","zoom.label":"Zoom controls","zoom.out":"Zoom out","zoom.in":"Zoom in","zoom.value":"Zoom level","zoom.fit":"Fit to screen","zoom.fitShort":"Fit","zoom.fullscreen":"Enter fullscreen","zoom.exitFullscreen":"Exit fullscreen","zoom.fullscreenError":"Fullscreen is unavailable.","toolPanel.close":"Close panel","toolPanel.tabs":"Panel tabs","toolLayout.resize.sidebar":"Resize sidebar width","toolLayout.resize.inspector":"Resize inspector width","toolLayout.resize.bottom":"Resize bottom panel height","password.weak":"Weak","password.fair":"Fair","password.good":"Strong","password.veryStrong":"Very strong","rating.high":"Very good","rating.medium":"Average","rating.low":"Low","rating.label":"Rating {value}","rating.review":"{count} review","rating.reviews":"{count} reviews","countdown.days":"Days","countdown.hours":"Hours","countdown.minutes":"Minutes","countdown.seconds":"Seconds","upload.title":"Drag files here or click to choose","upload.choose":"Choose files","upload.allFormats":"All file types supported","upload.maximum":" (up to {size})","upload.files":"Uploaded files ({count})","upload.retry":"Retry","upload.remove":"Remove","upload.illustration":"Upload illustration","creditCard.security":"Security code {code}","creditCard.label":"{issuer} card{lastFour}, flip","creditCard.ending":", ending in {digits}","creditCard.front":"Front","creditCard.back":"Back","creditCard.showing":"Showing {side}","creditCard.expiry":"Expires {expiry}","creditCard.show":"Press to show {side}","flight.scheduled":"Scheduled","flight.boarding":"Boarding","flight.in-flight":"In flight","flight.landed":"Landed","flight.delayed":"Delayed","flight.cancelled":"Cancelled","flight.title":"Flight status","flight.progress":"Journey progress","boardingPass.origin":"From","boardingPass.destination":"To","boardingPass.passenger":"Passenger","boardingPass.service":"Flight","boardingPass.date":"Date","boardingPass.boarding":"Boarding","boardingPass.departure":"Departure","boardingPass.arrival":"Arrival","boardingPass.terminal":"Terminal","boardingPass.gate":"Gate","boardingPass.seat":"Seat","boardingPass.travelClass":"Class","boardingPass.title":"Boarding pass","wallet.title":"My boarding passes","wallet.empty":"No boarding passes yet.","wallet.journeys":"Choose a journey","receipt.quantity":"Qty","receipt.subtotal":"Subtotal","receipt.discount":"Discount","receipt.tax":"Tax","receipt.total":"Total","receipt.payment":"Payment","receipt.empty":"No items to display.","receipt.invalid":"Check the items and amounts.","receipt.title":"Receipt","ticket.title":"Ticket","paper.codeImage":"Code image","aiElement.emptyFolder":"Empty folder","aiElement.noFiles":"No files to display.","attachment.title":"Attachments","attachment.empty":"No attachments yet.","attachment.queued":"Queued","attachment.uploading":"Uploading","attachment.processing":"Processing","attachment.ready":"Ready","attachment.error":"Failed","attachment.cancelled":"Cancelled","attachment.unknown":"Checking progress…","attachment.cancel":"Cancel","attachment.retry":"Retry","attachment.remove":"Remove from list","attachment.open":"View details","attachment.reload":"Reload","workspace.members":"Workspace members","workspace.invite":"Invite members","workspace.email":"Email","workspace.role":"Role","workspace.chooseRole":"Choose a role","workspace.description":"Review the email address and role of the person you want to invite.","workspace.submit":"Send invitation","workspace.emailError":"Enter a valid email address of up to 254 characters.","workspace.roleError":"Choose a role for the invitation.","workspace.retry":"Reload","workspace.reinvite":"Invite again","workspace.active":"Active","workspace.invited":"Invitation pending","workspace.expired":"Invitation expired","workspace.empty":"No members to display yet.","aiElement.pending":"Pending","aiElement.running":"In progress","aiElement.complete":"Complete","aiElement.error":"Failed","aiElement.cancelled":"Cancelled","aiElement.approval":"Awaiting approval","aiElement.researching":"Reviewing sources","aiElement.researched":"View research","aiElement.thinking":"Thinking","aiElement.reasoning":"Response summary","aiElement.plan":"Plan","aiElement.queue":"Queued requests","aiElement.remove":"Remove","aiElement.queueEmpty":"No queued requests.","aiElement.sources":"Sources","aiElement.citation":"Source","aiElement.checkpoint":"Checkpoint saved","aiElement.restore":"Restore from here","aiElement.confirmation":"Proceed with this action?","aiElement.allow":"Allow","aiElement.reject":"Reject","aiElement.accepted":"Action approved.","aiElement.rejected":"Action rejected.","aiElement.context":"Context","aiElement.tokens":"Tokens","aiElement.unknown":"Usage is not available yet.","aiElement.files":"Files","aiElement.task":"Task progress","aiElement.subagent":"Subagent","aiElement.tool":"Tool call","aiElement.terminal":"Terminal","aiElement.stackTrace":"Error details","aiElement.tests":"Test results","aiElement.passed":"Passed","aiElement.failed":"Failed","aiElement.input":"Input","aiElement.output":"Output","aiElement.user":"User","aiElement.assistant":"AI response","aiElement.copy":"Copy","aiElement.regenerate":"Regenerate","aiElement.like":"Like","aiElement.dislike":"Dislike","aiElement.latest":"Latest response","aiElement.conversation":"AI conversation","notification.title":"Notifications","notification.filters":"Notification filters","notification.all":"All","notification.unread":"Unread","notification.readAll":"Mark displayed notifications as read","notification.markRead":"Mark as read","notification.retry":"Reload","notification.empty":"No notifications yet.","notification.caughtUp":"You’re all caught up.","notification.countUnknown":"Needs review","aiModel.label":"Model","aiModel.choose":"Choose a model","aiModel.empty":"No models are available right now.","aiModel.capabilities":"Capabilities","aiModel.usage":"Usage unit","aiQuote.expires":"Quote expires","aiQuote.title":"Execution estimate","aiQuote.reserved":"Maximum credit reservation","aiQuote.providerCost":"Estimated provider cost","aiQuote.pricingVersion":"Pricing version","aiQuote.unknown":"Unconfirmed","aiQuote.idle":"Choose a model and enter a request to get an estimate.","aiQuote.loading":"Checking the estimate…","aiQuote.executing":"Processing your request. Do not submit it again.","aiQuote.stale":"The model or input has changed. Refresh the estimate.","aiQuote.expired":"The estimate has expired. Request a new estimate.","aiQuote.balanceUnknown":"Your balance is unavailable. Refresh to try again.","aiQuote.insufficient":"Additional credits needed:","aiQuote.ready":"Credits are reserved when execution starts and settled against actual usage when it finishes.","aiQuote.refresh":"Refresh estimate","aiQuote.request":"Get estimate","aiQuote.execute":"Run","plan.current":"Current plan","plan.usage":"Usage this period","plan.allowed":"Included","plan.locked":"Not included","plan.manage":"Manage plan","plan.retry":"Reload","history.title":"Credit history","history.empty":"No history yet.","history.unresolved":"Checking","history.grant":"Granted","history.charge":"Used","history.hold":"Reserved · not settled","history.release":"Reservation released","credits.choosePlan":"Credits to add","credits.rechargeDescription":"Choose an amount and review the price and terms.","credits.paymentAmount":"Payment amount","credits.paymentPending":"Checking payment status","credits.remaining":"Remaining credits","credits.available":"Available credits","credits.unit":"credits","credits.held":"Reserved","credits.spent":"Total used","credits.charged":"Charged","credits.low":"Your credit balance is running low.","credits.recharge":"Add credits","credits.checkout":"Continue to payment","credits.noPlans":"No credit packs are available right now.","aiTask.title":"AI task","aiTask.idle":"Not started","aiTask.queued":"Queued","aiTask.running":"Running","aiTask.succeeded":"Complete","aiTask.failed":"Failed","aiTask.cancelled":"Cancelled","aiTask.needs_attention":"Review needed","aiTask.attentionDescription":"We’re checking the result and usage. Reserved credits remain on hold until the review is complete.",close:"Close","modal.close":"Close modal","drawer.close":"Close drawer","dialog.close":"Close dialog","toast.close":"Dismiss notification",clear:"Clear",clearAll:"Clear all",loading:"Loading",required:"Required",optional:"Optional",error:"Error",success:"Success",warning:"Warning",info:"Information",previous:"Previous",next:"Next","select.placeholder":"Select…","select.noResults":"No results","navigationRail.label":"Main navigation","navigationRail.newNotification":"New notification","navigationRail.notifications":"Notifications","tabs.label":"Tabs","pagination.label":"Pagination","search.label":"Search","media.play":"Play","media.pause":"Pause","media.mute":"Mute","media.unmute":"Unmute","media.settings":"Settings","media.shuffle":"Shuffle","media.repeat":"Repeat","media.previous":"Previous track","media.next":"Next track","media.fullscreen":"Full screen","confirm.ok":"Confirm","confirm.cancel":"Cancel","carousel.slide":"slide","carousel.role":"carousel","carousel.previous":"Previous slide","carousel.next":"Next slide","breadcrumbs.expand":"Show hidden path","input.showPassword":"Show password","input.hidePassword":"Hide password","input.clearSearch":"Clear search","verify.done":"Verified","verify.phoneDone":"Your phone number has been verified.","bentoCard.cta":"View details","tweetCard.loading":"Loading post…","tweetCard.error":"Unable to load this post.","tweetCard.viewOnTwitter":"View on X","tweetCard.like":"Like","tweetCard.retweet":"Repost","tweetCard.reply":"Reply","tweetCard.verified":"Verified account","tweetCard.retry":"Try again","chatComposer.placeholder":"Type a message…","chatComposer.send":"Send","chatComposer.stop":"Stop generating","chatComposer.attach":"Attach","voice.start":"Voice input","voice.stop":"Finish dictation","voice.cancel":"Cancel voice input","voice.starting":"Connecting microphone","voice.listening":"Listening","voice.stopping":"Finishing transcription","voice.error":"Voice recognition failed","voice.unsupported":"Voice input not supported"};export{e as ENGLISH_STRINGS};
1
+ const e={"appUpdate.notice":"Update available","appUpdate.changes":"What’s new","appUpdate.automatic":"Restarts automatically when complete.","appUpdate.install":"Update","appUpdate.inProgress":"Updating","appUpdate.dialogTitle":"App update","appUpdate.toastComplete":"Updated to {version}.","appUpdate.checking.title":"Checking for updates","appUpdate.checking.description":"Looking for a new update.","appUpdate.up-to-date.title":"You’re up to date","appUpdate.up-to-date.description":"You’re using the latest available version.","appUpdate.available.title":"An update is available","appUpdate.available.description":"Download the latest improvements and stability updates.","appUpdate.downloading.title":"Downloading update","appUpdate.downloading.description":"We’ll let you know how to apply it when the download finishes.","appUpdate.ready.title":"Ready to update","appUpdate.ready.description":"Save your work, then restart the app to apply the update.","appUpdate.applying.title":"Applying update","appUpdate.applying.description":"Keep the app open and wait a moment.","appUpdate.complete.title":"Update complete","appUpdate.complete.description":"The new version has been applied successfully.","appUpdate.error.title":"Update could not be completed","appUpdate.error.description":"Check your connection and try again.","appUpdate.offline.title":"Internet connection required","appUpdate.offline.description":"Connect to the internet, then try again.","appUpdate.current":"Current","appUpdate.target":"New version","appUpdate.required":"Required update","appUpdate.later":"Later","appUpdate.download":"Download update","appUpdate.restart":"Restart to update","appUpdate.retry":"Try again","appUpdate.done":"Done","media.restoreHandles":"Restore handles (H)","media.tangentHelp":"Drag: symmetric · Cmd/Ctrl: independent · H: restore · dashed handles reveal collapsed controls.","media.resizeTimeline":"Resize timeline height","media.resizeTimelineHint":"Drag vertically to resize. Double-click or Home for automatic height.","media.resizeTrack":"Resize track height","easing.outVelocity":"Out velocity ×","easing.inVelocity":"In velocity ×","easing.label":"Easing & velocity","easing.graphMode":"Graph mode","easing.value":"Value","easing.speed":"Speed","easing.valueGraph":"Normalized progress curve","easing.speedGraph":"Normalized velocity graph","easing.custom":"Custom curve","easing.presets":"Easing presets","easing.linear":"Linear","easing.ease":"Ease","easing.ease-in":"Ease in","easing.ease-out":"Ease out","easing.ease-in-out":"Ease in/out","easing.smooth":"Smooth","easing.snappy":"Snappy","easing.swift":"Swift out","easing.gentle":"Gentle","easing.emphasized":"Emphasized","easing.outgoing":"Outgoing tangent","easing.incoming":"Incoming tangent","easing.preview":"Play easing preview","easing.velocityHint":"1× = constant speed","easing.speedHelp":"Drag tangent controls to change influence and endpoint velocity. Vertical scale is normalized; multiply by value change / duration for units per second.","easing.valueHelp":"Drag handles or enter exact coordinates. Arrow keys nudge .01; Shift nudges .1. Escape cancels dragging.","media.trimStart":"Trim start","media.trimEnd":"Trim end","media.fadeIn":"Fade in","media.fadeOut":"Fade out","media.effects":"Applied effects","media.timeline":"Video timeline","media.tracks":"Tracks","media.more":"Scroll to more tracks and clips","media.lock":"Lock track","media.trackLocked":"Locked","media.trackUnlocked":"Unlocked","media.soundOn":"Sound on","media.soundOff":"Muted","media.marker":"Timeline marker","media.trackMute":"Mute track","media.duration":"Duration","media.playhead":"Playhead","media.emptyTracks":"No tracks. Connect a tracks array to begin.","media.keyframes":"Keyframes","media.curve":"Value graph","media.previousKey":"Previous keyframe","media.nextKey":"Next keyframe","media.addKey":"Add keyframe (K)","media.deleteKey":"Delete keyframe","media.outHandle":"Outgoing Bezier handle","media.inHandle":"Incoming Bezier handle","media.keyframe":"Keyframe","media.emptyKeys":"Add a keyframe or double-click the graph.","media.selectKey":"Select a keyframe to edit its time, value and interpolation.","media.time":"Time (seconds)","media.value":"Value","media.interpolation":"Interpolation","media.linear":"Linear","media.hold":"Hold","media.ease-in":"Ease in","media.ease-out":"Ease out","media.ease-in-out":"Ease in / out","media.bezier":"Custom Bezier","selectionBar.count":"{count} selected","selectionBar.clear":"Clear","selectionBar.label":"Selection actions","zoom.label":"Zoom controls","zoom.out":"Zoom out","zoom.in":"Zoom in","zoom.value":"Zoom level","zoom.fit":"Fit to screen","zoom.fitShort":"Fit","zoom.fullscreen":"Enter fullscreen","zoom.exitFullscreen":"Exit fullscreen","zoom.fullscreenError":"Fullscreen is unavailable.","toolPanel.close":"Close panel","toolPanel.tabs":"Panel tabs","toolLayout.resize.sidebar":"Resize sidebar width","toolLayout.resize.inspector":"Resize inspector width","toolLayout.resize.bottom":"Resize bottom panel height","password.weak":"Weak","password.fair":"Fair","password.good":"Strong","password.veryStrong":"Very strong","rating.high":"Very good","rating.medium":"Average","rating.low":"Low","rating.label":"Rating {value}","rating.review":"{count} review","rating.reviews":"{count} reviews","countdown.days":"Days","countdown.hours":"Hours","countdown.minutes":"Minutes","countdown.seconds":"Seconds","upload.title":"Drag files here or click to choose","upload.choose":"Choose files","upload.allFormats":"All file types supported","upload.maximum":" (up to {size})","upload.files":"Uploaded files ({count})","upload.retry":"Retry","upload.remove":"Remove","upload.illustration":"Upload illustration","creditCard.security":"Security code {code}","creditCard.label":"{issuer} card{lastFour}, flip","creditCard.ending":", ending in {digits}","creditCard.front":"Front","creditCard.back":"Back","creditCard.showing":"Showing {side}","creditCard.expiry":"Expires {expiry}","creditCard.show":"Press to show {side}","flight.scheduled":"Scheduled","flight.boarding":"Boarding","flight.in-flight":"In flight","flight.landed":"Landed","flight.delayed":"Delayed","flight.cancelled":"Cancelled","flight.title":"Flight status","flight.progress":"Journey progress","boardingPass.origin":"From","boardingPass.destination":"To","boardingPass.passenger":"Passenger","boardingPass.service":"Flight","boardingPass.date":"Date","boardingPass.boarding":"Boarding","boardingPass.departure":"Departure","boardingPass.arrival":"Arrival","boardingPass.terminal":"Terminal","boardingPass.gate":"Gate","boardingPass.seat":"Seat","boardingPass.travelClass":"Class","boardingPass.title":"Boarding pass","wallet.title":"My boarding passes","wallet.empty":"No boarding passes yet.","wallet.journeys":"Choose a journey","receipt.quantity":"Qty","receipt.subtotal":"Subtotal","receipt.discount":"Discount","receipt.tax":"Tax","receipt.total":"Total","receipt.payment":"Payment","receipt.empty":"No items to display.","receipt.invalid":"Check the items and amounts.","receipt.title":"Receipt","ticket.title":"Ticket","paper.codeImage":"Code image","aiElement.emptyFolder":"Empty folder","aiElement.noFiles":"No files to display.","attachment.title":"Attachments","attachment.empty":"No attachments yet.","attachment.queued":"Queued","attachment.uploading":"Uploading","attachment.processing":"Processing","attachment.ready":"Ready","attachment.error":"Failed","attachment.cancelled":"Cancelled","attachment.unknown":"Checking progress…","attachment.cancel":"Cancel","attachment.retry":"Retry","attachment.remove":"Remove from list","attachment.open":"View details","attachment.reload":"Reload","workspace.members":"Workspace members","workspace.invite":"Invite members","workspace.email":"Email","workspace.role":"Role","workspace.chooseRole":"Choose a role","workspace.description":"Review the email address and role of the person you want to invite.","workspace.submit":"Send invitation","workspace.emailError":"Enter a valid email address of up to 254 characters.","workspace.roleError":"Choose a role for the invitation.","workspace.retry":"Reload","workspace.reinvite":"Invite again","workspace.active":"Active","workspace.invited":"Invitation pending","workspace.expired":"Invitation expired","workspace.empty":"No members to display yet.","aiElement.pending":"Pending","aiElement.running":"In progress","aiElement.complete":"Complete","aiElement.error":"Failed","aiElement.cancelled":"Cancelled","aiElement.approval":"Awaiting approval","aiElement.researching":"Reviewing sources","aiElement.researched":"View research","aiElement.thinking":"Thinking","aiElement.reasoning":"Response summary","aiElement.plan":"Plan","aiElement.queue":"Queued requests","aiElement.remove":"Remove","aiElement.queueEmpty":"No queued requests.","aiElement.sources":"Sources","aiElement.citation":"Source","aiElement.checkpoint":"Checkpoint saved","aiElement.restore":"Restore from here","aiElement.confirmation":"Proceed with this action?","aiElement.allow":"Allow","aiElement.reject":"Reject","aiElement.accepted":"Action approved.","aiElement.rejected":"Action rejected.","aiElement.context":"Context","aiElement.tokens":"Tokens","aiElement.unknown":"Usage is not available yet.","aiElement.files":"Files","aiElement.task":"Task progress","aiElement.subagent":"Subagent","aiElement.tool":"Tool call","aiElement.terminal":"Terminal","aiElement.stackTrace":"Error details","aiElement.tests":"Test results","aiElement.passed":"Passed","aiElement.failed":"Failed","aiElement.input":"Input","aiElement.output":"Output","aiElement.user":"User","aiElement.assistant":"AI response","aiElement.copy":"Copy","aiElement.regenerate":"Regenerate","aiElement.like":"Like","aiElement.dislike":"Dislike","aiElement.latest":"Latest response","aiElement.conversation":"AI conversation","notification.title":"Notifications","notification.filters":"Notification filters","notification.all":"All","notification.unread":"Unread","notification.readAll":"Mark displayed notifications as read","notification.markRead":"Mark as read","notification.retry":"Reload","notification.empty":"No notifications yet.","notification.caughtUp":"You’re all caught up.","notification.countUnknown":"Needs review","aiModel.label":"Model","aiModel.choose":"Choose a model","aiModel.empty":"No models are available right now.","aiModel.capabilities":"Capabilities","aiModel.usage":"Usage unit","aiQuote.expires":"Quote expires","aiQuote.title":"Execution estimate","aiQuote.reserved":"Maximum credit reservation","aiQuote.providerCost":"Estimated provider cost","aiQuote.pricingVersion":"Pricing version","aiQuote.unknown":"Unconfirmed","aiQuote.idle":"Choose a model and enter a request to get an estimate.","aiQuote.loading":"Checking the estimate…","aiQuote.executing":"Processing your request. Do not submit it again.","aiQuote.stale":"The model or input has changed. Refresh the estimate.","aiQuote.expired":"The estimate has expired. Request a new estimate.","aiQuote.balanceUnknown":"Your balance is unavailable. Refresh to try again.","aiQuote.insufficient":"Additional credits needed:","aiQuote.ready":"Credits are reserved when execution starts and settled against actual usage when it finishes.","aiQuote.refresh":"Refresh estimate","aiQuote.request":"Get estimate","aiQuote.execute":"Run","plan.current":"Current plan","plan.usage":"Usage this period","plan.allowed":"Included","plan.locked":"Not included","plan.manage":"Manage plan","plan.retry":"Reload","history.title":"Credit history","history.empty":"No history yet.","history.unresolved":"Checking","history.grant":"Granted","history.charge":"Used","history.hold":"Reserved · not settled","history.release":"Reservation released","credits.choosePlan":"Credits to add","credits.rechargeDescription":"Choose an amount and review the price and terms.","credits.paymentAmount":"Payment amount","credits.paymentPending":"Checking payment status","credits.remaining":"Remaining credits","credits.available":"Available credits","credits.unit":"credits","credits.held":"Reserved","credits.spent":"Total used","credits.charged":"Charged","credits.low":"Your credit balance is running low.","credits.recharge":"Add credits","credits.checkout":"Continue to payment","credits.noPlans":"No credit packs are available right now.","aiTask.title":"AI task","aiTask.idle":"Not started","aiTask.queued":"Queued","aiTask.running":"Running","aiTask.succeeded":"Complete","aiTask.failed":"Failed","aiTask.cancelled":"Cancelled","aiTask.needs_attention":"Review needed","aiTask.attentionDescription":"We’re checking the result and usage. Reserved credits remain on hold until the review is complete.",close:"Close","modal.close":"Close modal","drawer.close":"Close drawer","dialog.close":"Close dialog","toast.close":"Dismiss notification",clear:"Clear",clearAll:"Clear all",loading:"Loading",required:"Required",optional:"Optional",error:"Error",success:"Success",warning:"Warning",info:"Information",previous:"Previous",next:"Next","select.placeholder":"Select…","select.noResults":"No results","navigationRail.label":"Main navigation","navigationRail.newNotification":"New notification","navigationRail.notifications":"Notifications","tabs.label":"Tabs","pagination.label":"Pagination","search.label":"Search","media.play":"Play","media.pause":"Pause","media.mute":"Mute","media.unmute":"Unmute","media.settings":"Settings","media.shuffle":"Shuffle","media.repeat":"Repeat","media.previous":"Previous track","media.next":"Next track","media.fullscreen":"Full screen","confirm.ok":"Confirm","confirm.cancel":"Cancel","carousel.slide":"slide","carousel.role":"carousel","carousel.previous":"Previous slide","carousel.next":"Next slide","breadcrumbs.expand":"Show hidden path","input.showPassword":"Show password","input.hidePassword":"Hide password","input.clearSearch":"Clear search","verify.done":"Verified","verify.phoneDone":"Your phone number has been verified.","bentoCard.cta":"View details","tweetCard.loading":"Loading post…","tweetCard.error":"Unable to load this post.","tweetCard.viewOnTwitter":"View on X","tweetCard.like":"Like","tweetCard.retweet":"Repost","tweetCard.reply":"Reply","tweetCard.verified":"Verified account","tweetCard.retry":"Try again","chatComposer.placeholder":"Type a message…","chatComposer.send":"Send","chatComposer.stop":"Stop generating","chatComposer.attach":"Attach","voice.start":"Voice input","voice.stop":"Finish dictation","voice.cancel":"Cancel voice input","voice.starting":"Connecting microphone","voice.listening":"Listening","voice.stopping":"Finishing transcription","voice.error":"Voice recognition failed","voice.unsupported":"Voice input not supported"};export{e as ENGLISH_STRINGS};
@@ -11,6 +11,108 @@
11
11
  */
12
12
  /** 컴포넌트가 쓰는 문자열 키 → 기본(한국어) 값 */
13
13
  declare const DEFAULT_STRINGS: {
14
+ readonly "appUpdate.notice": "업데이트 안내";
15
+ readonly "appUpdate.changes": "주요 변경 내용";
16
+ readonly "appUpdate.automatic": "완료 후 자동으로 다시 시작됩니다.";
17
+ readonly "appUpdate.install": "업데이트";
18
+ readonly "appUpdate.inProgress": "업데이트 중";
19
+ readonly "appUpdate.dialogTitle": "앱 업데이트";
20
+ readonly "appUpdate.toastComplete": "{version} 업데이트가 완료됐어요.";
21
+ readonly "appUpdate.checking.title": "업데이트 확인 중";
22
+ readonly "appUpdate.checking.description": "새로운 업데이트가 있는지 확인하고 있습니다.";
23
+ readonly "appUpdate.up-to-date.title": "최신 버전입니다";
24
+ readonly "appUpdate.up-to-date.description": "현재 사용할 수 있는 최신 버전을 이용 중입니다.";
25
+ readonly "appUpdate.available.title": "새로운 업데이트가 있어요";
26
+ readonly "appUpdate.available.description": "개선된 기능과 안정성 업데이트를 다운로드할 수 있습니다.";
27
+ readonly "appUpdate.downloading.title": "업데이트 다운로드 중";
28
+ readonly "appUpdate.downloading.description": "다운로드가 끝나면 적용 방법을 안내해 드립니다.";
29
+ readonly "appUpdate.ready.title": "업데이트를 적용할 준비가 됐어요";
30
+ readonly "appUpdate.ready.description": "작업을 저장한 뒤 앱을 다시 시작해 업데이트를 적용하세요.";
31
+ readonly "appUpdate.applying.title": "업데이트 적용 중";
32
+ readonly "appUpdate.applying.description": "앱을 닫지 말고 잠시 기다려 주세요.";
33
+ readonly "appUpdate.complete.title": "업데이트가 완료됐어요";
34
+ readonly "appUpdate.complete.description": "새 버전이 정상적으로 적용되었습니다.";
35
+ readonly "appUpdate.error.title": "업데이트를 완료하지 못했어요";
36
+ readonly "appUpdate.error.description": "연결 상태를 확인하고 다시 시도해 주세요.";
37
+ readonly "appUpdate.offline.title": "인터넷 연결이 필요해요";
38
+ readonly "appUpdate.offline.description": "인터넷에 연결한 뒤 업데이트를 다시 시도해 주세요.";
39
+ readonly "appUpdate.current": "현재";
40
+ readonly "appUpdate.target": "새 버전";
41
+ readonly "appUpdate.required": "필수 업데이트";
42
+ readonly "appUpdate.later": "나중에";
43
+ readonly "appUpdate.download": "업데이트 다운로드";
44
+ readonly "appUpdate.restart": "다시 시작하여 적용";
45
+ readonly "appUpdate.retry": "다시 시도";
46
+ readonly "appUpdate.done": "확인";
47
+ readonly "media.restoreHandles": "핸들 복구 (H)";
48
+ readonly "media.tangentHelp": "드래그: 대칭 · Cmd/Ctrl: 개별 · H: 복구 · 겹친 핸들은 점선으로 꺼내 표시합니다.";
49
+ readonly "media.resizeTimeline": "타임라인 높이 조절";
50
+ readonly "media.resizeTimelineHint": "위아래 드래그로 높이 조절. 두 번 클릭 또는 Home으로 자동 높이 복원.";
51
+ readonly "media.resizeTrack": "트랙 높이 조절";
52
+ readonly "easing.outVelocity": "나가는 속도 ×";
53
+ readonly "easing.inVelocity": "들어오는 속도 ×";
54
+ readonly "easing.label": "이징 · 속도";
55
+ readonly "easing.graphMode": "그래프 종류";
56
+ readonly "easing.value": "값 곡선";
57
+ readonly "easing.speed": "속도";
58
+ readonly "easing.valueGraph": "정규화된 진행 곡선";
59
+ readonly "easing.speedGraph": "정규화된 속도 그래프";
60
+ readonly "easing.custom": "사용자 곡선";
61
+ readonly "easing.presets": "이징 프리셋";
62
+ readonly "easing.linear": "선형";
63
+ readonly "easing.ease": "기본 이징";
64
+ readonly "easing.ease-in": "시작 가속";
65
+ readonly "easing.ease-out": "종료 감속";
66
+ readonly "easing.ease-in-out": "가속 · 감속";
67
+ readonly "easing.smooth": "부드럽게";
68
+ readonly "easing.snappy": "빠른 전환";
69
+ readonly "easing.swift": "빠른 진입";
70
+ readonly "easing.gentle": "은은하게";
71
+ readonly "easing.emphasized": "강조";
72
+ readonly "easing.outgoing": "나가는 접선";
73
+ readonly "easing.incoming": "들어오는 접선";
74
+ readonly "easing.preview": "이징 미리보기 재생";
75
+ readonly "easing.velocityHint": "1× = 일정한 속도";
76
+ readonly "easing.speedHelp": "접선 핸들로 영향 범위와 끝점 속도를 조정합니다. 실제 초당 속도는 정규화된 값에 값 변화량 / 구간 시간을 곱합니다.";
77
+ readonly "easing.valueHelp": "핸들을 드래그하거나 좌표를 입력하세요. 방향키 .01, Shift .1 단위 조정, Esc 이동 취소.";
78
+ readonly "media.trimStart": "시작 트림";
79
+ readonly "media.trimEnd": "끝 트림";
80
+ readonly "media.fadeIn": "페이드 인";
81
+ readonly "media.fadeOut": "페이드 아웃";
82
+ readonly "media.effects": "적용 효과";
83
+ readonly "media.timeline": "영상 타임라인";
84
+ readonly "media.tracks": "트랙";
85
+ readonly "media.more": "오른쪽 클립 더 보기";
86
+ readonly "media.lock": "트랙 잠금";
87
+ readonly "media.trackLocked": "잠김";
88
+ readonly "media.trackUnlocked": "잠금 해제됨";
89
+ readonly "media.soundOn": "소리 켜짐";
90
+ readonly "media.soundOff": "음소거됨";
91
+ readonly "media.marker": "타임라인 마커";
92
+ readonly "media.trackMute": "트랙 음소거";
93
+ readonly "media.duration": "길이";
94
+ readonly "media.playhead": "재생 위치";
95
+ readonly "media.emptyTracks": "트랙 배열을 연결해 시작하세요.";
96
+ readonly "media.keyframes": "키프레임";
97
+ readonly "media.curve": "값 그래프";
98
+ readonly "media.previousKey": "이전 키프레임";
99
+ readonly "media.nextKey": "다음 키프레임";
100
+ readonly "media.addKey": "키프레임 추가 (K)";
101
+ readonly "media.deleteKey": "키프레임 삭제";
102
+ readonly "media.outHandle": "나가는 베지어 핸들";
103
+ readonly "media.inHandle": "들어오는 베지어 핸들";
104
+ readonly "media.keyframe": "키프레임";
105
+ readonly "media.emptyKeys": "키프레임을 추가하거나 그래프를 두 번 클릭하세요.";
106
+ readonly "media.selectKey": "키프레임을 선택해 시간·값·보간을 편집하세요.";
107
+ readonly "media.time": "시간 (초)";
108
+ readonly "media.value": "값";
109
+ readonly "media.interpolation": "보간";
110
+ readonly "media.linear": "선형";
111
+ readonly "media.hold": "유지";
112
+ readonly "media.ease-in": "서서히 시작";
113
+ readonly "media.ease-out": "서서히 종료";
114
+ readonly "media.ease-in-out": "서서히 시작 / 종료";
115
+ readonly "media.bezier": "사용자 베지어";
14
116
  readonly 'selectionBar.count': "{count}개 선택";
15
117
  readonly 'selectionBar.clear': "해제";
16
118
  readonly 'selectionBar.label': "선택 항목 작업";