pict-editor-timeline 0.0.1

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/DESIGN.md ADDED
@@ -0,0 +1,307 @@
1
+ # pict-editor-timeline — Design Spec
2
+
3
+ ## Purpose
4
+
5
+ A standalone Pict view library that renders a visual timeline editor
6
+ for building multi-beat video storyboards. Users add, remove, reorder,
7
+ and configure "cuts" — each cut has a text prompt, a duration, and
8
+ optional start/end frame image slots. The editor exports a JSON array
9
+ that any downstream system (retold-labs, ComfyUI, a CLI tool) can
10
+ consume without knowing about the editor.
11
+
12
+ The library has **zero knowledge** of retold-labs, Ultravisor, model
13
+ weights, or video generation. It is a pure UI component for
14
+ assembling a sequence of annotated time segments with optional image
15
+ references.
16
+
17
+ ## Module location
18
+
19
+ ```
20
+ retold/modules/pict/pict-editor-timeline/
21
+ ```
22
+
23
+ Standard Pict ecosystem module alongside `pict-section-form`,
24
+ `pict-section-formeditor`, `pict-provider-list`, etc.
25
+
26
+ ## Data model
27
+
28
+ The timeline editor operates on a single data structure: an ordered
29
+ array of **cuts**. Each cut is a plain object:
30
+
31
+ ```javascript
32
+ {
33
+ // Identity
34
+ id: "cut-0", // Auto-assigned, stable across reorders
35
+
36
+ // Content
37
+ prompt: "A woman walks through a garden at golden hour",
38
+ target_seconds: 3,
39
+
40
+ // Image references (opaque strings — could be file paths,
41
+ // data URLs, LabAsset GUIDs, or anything the host app provides)
42
+ start_image: "", // First frame reference
43
+ end_image: "", // Last frame reference (optional)
44
+
45
+ // UI state (not exported)
46
+ _collapsed: false,
47
+ _dragOver: false
48
+ }
49
+ ```
50
+
51
+ ### Export format
52
+
53
+ The `getStoryboard()` method returns a clean JSON array with only the
54
+ fields the storyboard workflow consumes:
55
+
56
+ ```json
57
+ [
58
+ {
59
+ "prompt": "A woman walks through a garden at golden hour",
60
+ "target_seconds": 3,
61
+ "beat_image": "/path/to/garden.jpg"
62
+ },
63
+ {
64
+ "prompt": "She bends down to pick a flower",
65
+ "target_seconds": 2
66
+ }
67
+ ]
68
+ ```
69
+
70
+ Mapping: `start_image` → `beat_image` (the storyboard worker's
71
+ field name). `end_image` is advisory — it helps the user visualize
72
+ the transition but doesn't affect generation today. When VACE
73
+ extension mode eventually supports end-frame targeting, the field
74
+ is ready.
75
+
76
+ ### Import format
77
+
78
+ The `loadStoryboard(jsonArray)` method accepts the same format and
79
+ populates the timeline from it. Round-trip: export → import →
80
+ export produces identical JSON.
81
+
82
+ ## Architecture
83
+
84
+ ### File structure
85
+
86
+ ```
87
+ pict-editor-timeline/
88
+ ├── package.json
89
+ ├── .quackage.json
90
+ ├── source/
91
+ │ ├── Pict-Section-Timeline.js # Main export
92
+ │ ├── views/
93
+ │ │ ├── PictView-Timeline.js # Container view: renders the full editor
94
+ │ │ ├── PictView-Timeline-Cut.js # Per-cut row: prompt, duration, image slots
95
+ │ │ └── PictView-Timeline-Toolbar.js # Top bar: add cut, import/export, total duration
96
+ │ ├── providers/
97
+ │ │ ├── Pict-Provider-TimelineDragDrop.js # HTML5 drag-and-drop reordering
98
+ │ │ └── Pict-Provider-TimelineOps.js # Data mutations (add, remove, reorder, update)
99
+ │ └── templates/
100
+ │ ├── timeline-container.html
101
+ │ ├── timeline-cut.html
102
+ │ └── timeline-toolbar.html
103
+ ├── test/
104
+ │ └── Pict-Timeline_tests.js
105
+ └── docs/
106
+ └── README.md
107
+ ```
108
+
109
+ ### View hierarchy
110
+
111
+ ```
112
+ PictView-Timeline (container)
113
+ ├── PictView-Timeline-Toolbar
114
+ │ ├── [+ Add Cut] button
115
+ │ ├── [Import JSON] button
116
+ │ ├── [Export JSON] button
117
+ │ └── Total duration display (sum of all cuts' target_seconds)
118
+
119
+ ├── PictView-Timeline-Cut (repeated per cut, vertical list)
120
+ │ ├── Drag handle (≡)
121
+ │ ├── Cut number badge (#1, #2, ...)
122
+ │ ├── Start frame image slot (drop zone / upload / paste)
123
+ │ ├── Prompt textarea
124
+ │ ├── Duration control (target_seconds, number input with +/- steppers)
125
+ │ ├── End frame image slot (drop zone / upload / paste, optional)
126
+ │ ├── [Duplicate] [Delete] buttons
127
+ │ └── Collapse/expand toggle
128
+
129
+ └── Visual timeline strip (bottom, read-only)
130
+ └── Proportional-width color blocks per cut showing relative durations
131
+ ```
132
+
133
+ ### Provider pattern
134
+
135
+ Following `pict-section-formeditor` exactly:
136
+
137
+ **PictView-Timeline** (main view):
138
+ - Extends `libPictView`
139
+ - In constructor: creates `TimelineDragDrop` and `TimelineOps`
140
+ providers via `this.pict.addProvider()`
141
+ - Stores the cut array in `this.pict.AppData.Timeline.Cuts`
142
+ - Renders by iterating over cuts and emitting per-cut HTML
143
+ - Exposes `getStoryboard()` and `loadStoryboard()` on `window`
144
+
145
+ **Pict-Provider-TimelineDragDrop**:
146
+ - Extends `libPictProvider`
147
+ - Tracks `_DragState` with source cut index
148
+ - Implements `onDragStart`, `onDragOver`, `onDrop`, `onDragEnd`
149
+ - Uses top/bottom half detection for insert-before/insert-after
150
+ - Calls `this._ParentTimeline.render()` after reorder
151
+
152
+ **Pict-Provider-TimelineOps**:
153
+ - Extends `libPictProvider`
154
+ - `addCut(afterIndex)` — insert a new cut with defaults
155
+ - `removeCut(index)` — remove and re-index
156
+ - `duplicateCut(index)` — deep-copy + insert after
157
+ - `updateCut(index, field, value)` — update a single field
158
+ - `moveCut(fromIndex, toIndex)` — reorder (called by drag-drop)
159
+ - Always calls `this._ParentTimeline.render()` after mutations
160
+
161
+ ### Image slot design
162
+
163
+ Each cut has two image slots: start frame and end frame. The slots
164
+ are generic drop zones that support three input methods:
165
+
166
+ 1. **Click to browse** — triggers a hidden `<input type="file">`
167
+ 2. **Drag-and-drop** — accepts image files dropped from Finder/Explorer
168
+ 3. **Paste** — accepts clipboard image data (Ctrl/Cmd+V when focused)
169
+
170
+ When an image is provided via any method, the slot:
171
+ - Shows a thumbnail preview (resized client-side for display)
172
+ - Stores the image reference in the cut data
173
+
174
+ **The image reference format depends on the host app's adapter:**
175
+
176
+ | Host | start_image / end_image value |
177
+ |---|---|
178
+ | Standalone (no adapter) | Data URL (`data:image/png;base64,...`) |
179
+ | retold-labs adapter | Materialized asset path (`/path/to/materialized/GUID/file.jpg`) |
180
+ | CLI tool | Filesystem path (`./images/garden.jpg`) |
181
+
182
+ The timeline editor ships a default adapter that uses data URLs. The
183
+ host app overrides this by setting `options.ImageAdapter` to an object
184
+ with:
185
+
186
+ ```javascript
187
+ {
188
+ // Called when user drops/selects/pastes an image
189
+ onImageProvided: async (file, cutIndex, slot) => {
190
+ // Upload to storage, return a reference string
191
+ return "/path/to/stored/image.jpg";
192
+ },
193
+
194
+ // Called to render a thumbnail from a reference string
195
+ getThumbnailUrl: (reference) => {
196
+ // Return a URL the browser can display in an <img> tag
197
+ return reference; // file paths, data URLs, and http URLs all work
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### CSS approach
203
+
204
+ Dark theme by default (matching retold-labs). All classes prefixed
205
+ with `pet-` (Pict Editor Timeline). CSS injected via the PictView
206
+ `CSS` option so it's included in the Pict CSS cascade without
207
+ external stylesheets.
208
+
209
+ Key visual elements:
210
+ - Timeline container: vertical stack of cut cards
211
+ - Cut card: horizontal layout with drag handle | image | prompt | duration | image
212
+ - Image slots: dashed-border drop zones, thumbnail preview when populated
213
+ - Duration strip: horizontal bar at the bottom, each cut proportional to its duration
214
+ - Drag feedback: ghost opacity on drag source, insertion indicator on target
215
+
216
+ ### Default configuration
217
+
218
+ ```javascript
219
+ module.exports = {
220
+ ViewIdentifier: 'Pict-Editor-Timeline',
221
+ DefaultRenderable: 'Timeline-Container',
222
+ DefaultDestinationAddress: '#PictEditorTimeline',
223
+ AutoInitialize: false,
224
+ AutoRender: false,
225
+ CSS: '/* ... */',
226
+ CSSHash: 'View-Editor-Timeline',
227
+ CSSPriority: 500,
228
+ DefaultCut: {
229
+ prompt: '',
230
+ target_seconds: 2,
231
+ start_image: '',
232
+ end_image: ''
233
+ },
234
+ MaxCuts: 50,
235
+ MinTargetSeconds: 0.5,
236
+ MaxTargetSeconds: 30,
237
+ ImageAdapter: null // null = use built-in data-URL adapter
238
+ };
239
+ ```
240
+
241
+ ## retold-labs integration (separate from pict-editor-timeline)
242
+
243
+ When retold-labs imports the timeline editor, it provides:
244
+
245
+ 1. An `ImageAdapter` that wires image slots to the existing
246
+ asset-picker widget (upload → Parime → materialized path)
247
+ 2. A "Generate" button in the toolbar that serializes the timeline
248
+ to the storyboard format and POSTs it to the storyboard API
249
+ 3. A route (`#/timeline`) and nav item ("Timeline") in the sidebar
250
+
251
+ This integration code lives in retold-labs, NOT in
252
+ pict-editor-timeline. The library stays decoupled.
253
+
254
+ ```javascript
255
+ // In Pict-Application-RetoldLabs.js:
256
+ const libPictEditorTimeline = require('pict-editor-timeline');
257
+
258
+ this.pict.addView('Timeline-Editor', Object.assign(
259
+ {},
260
+ libPictEditorTimeline.default_configuration,
261
+ {
262
+ DefaultDestinationAddress: '#RetoldLabs-View-Timeline',
263
+ ImageAdapter: {
264
+ onImageProvided: async (file, cutIndex, slot) => {
265
+ // Upload to Parime, return materialized path
266
+ },
267
+ getThumbnailUrl: (ref) => ref
268
+ }
269
+ }),
270
+ libPictEditorTimeline);
271
+ ```
272
+
273
+ ## Build and test
274
+
275
+ ### Build
276
+ ```bash
277
+ npx quack build
278
+ ```
279
+ Produces `dist/pict-editor-timeline.js` (UMD bundle) for direct
280
+ `<script>` inclusion, plus the npm-requireable source tree.
281
+
282
+ ### Test
283
+ ```bash
284
+ npm test
285
+ ```
286
+ Mocha TDD tests covering:
287
+ - Data model: add/remove/reorder/duplicate/update cuts
288
+ - Export: getStoryboard() produces correct JSON
289
+ - Import: loadStoryboard() round-trips cleanly
290
+ - Duration math: total duration updates on add/remove/change
291
+ - Validation: prompt required, target_seconds in range
292
+
293
+ No browser tests in the library itself — the visual testing happens
294
+ in retold-labs' browser integration suite after integration.
295
+
296
+ ## Implementation order
297
+
298
+ 1. **Scaffold**: package.json, .quackage.json, directory structure
299
+ 2. **Data model + ops provider**: cut CRUD, reorder, export/import
300
+ 3. **Unit tests**: data model round-trip, duration math
301
+ 4. **Container view + toolbar**: render shell, add/export buttons
302
+ 5. **Cut view**: prompt textarea, duration control, image slots
303
+ 6. **Drag-and-drop provider**: reorder cuts by dragging
304
+ 7. **Image adapter**: default data-URL adapter, slot thumbnails
305
+ 8. **Duration strip**: proportional-width visual timeline at bottom
306
+ 9. **CSS**: dark theme, drag feedback, responsive layout
307
+ 10. **retold-labs integration**: ImageAdapter, route, nav item, Generate button
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "pict-editor-timeline",
3
+ "version": "0.0.1",
4
+ "description": "Pict visual timeline editor for building multi-beat video storyboards. Add, reorder, and configure cuts with text prompts, durations, and optional start/end frame image slots. Exports clean JSON consumable by any downstream video generation system.",
5
+ "main": "source/Pict-Section-Timeline.js",
6
+ "scripts": {
7
+ "start": "node source/Pict-Section-Timeline.js",
8
+ "test": "npx mocha -u tdd --exit --timeout 5000 test/Pict-Timeline_tests.js",
9
+ "tests": "npx mocha -u tdd --exit --timeout 5000 -g",
10
+ "coverage": "npx nyc --reporter=lcov --reporter=text npm test",
11
+ "build": "npx quack build"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/stevenvelozo/pict-editor-timeline.git"
16
+ },
17
+ "author": "steven velozo <steven@velozo.com>",
18
+ "license": "MIT",
19
+ "bugs": {
20
+ "url": "https://github.com/stevenvelozo/pict-editor-timeline/issues"
21
+ },
22
+ "homepage": "https://github.com/stevenvelozo/pict-editor-timeline#readme",
23
+ "dependencies": {
24
+ "pict-view": "^1.0.68",
25
+ "pict-provider": "^1.0.12"
26
+ },
27
+ "devDependencies": {
28
+ "fable": "^3.0.0",
29
+ "mocha": "^10.2.0",
30
+ "quackage": "^1.1.0"
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * pict-editor-timeline — Main Export
3
+ *
4
+ * Visual timeline editor for building multi-beat video storyboards.
5
+ * Exports the PictView-Timeline view class as the default export,
6
+ * plus individual provider classes for advanced usage.
7
+ *
8
+ * Usage:
9
+ * const libTimeline = require('pict-editor-timeline');
10
+ * pict.addView('MyTimeline', libTimeline.default_configuration, libTimeline);
11
+ *
12
+ * @author Steven Velozo <steven@velozo.com>
13
+ * @license MIT
14
+ */
15
+
16
+ const libPictViewTimeline = require('./views/PictView-Timeline.js');
17
+
18
+ // Default export: the main view class
19
+ module.exports = libPictViewTimeline;
20
+
21
+ // Re-export configuration and provider classes for advanced usage
22
+ module.exports.default_configuration = libPictViewTimeline.default_configuration;
23
+ module.exports.TimelineOpsProvider = require('./providers/Pict-Provider-TimelineOps.js');
24
+ module.exports.TimelineDragDropProvider = require('./providers/Pict-Provider-TimelineDragDrop.js');
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Pict Provider: Timeline Drag and Drop
3
+ *
4
+ * Handles HTML5 drag-and-drop reordering of cuts in the timeline.
5
+ * Follows the same pattern as pict-section-formeditor's
6
+ * Pict-Provider-FormEditorDragDrop: tracks drag state, detects
7
+ * insert position (before/after based on cursor Y), and delegates
8
+ * the actual array mutation to TimelineOps.moveCut().
9
+ *
10
+ * @author Steven Velozo <steven@velozo.com>
11
+ * @license MIT
12
+ */
13
+ const libPictProvider = require('pict-provider');
14
+
15
+ class PictProviderTimelineDragDrop extends libPictProvider
16
+ {
17
+ constructor(pFable, pOptions, pServiceHash)
18
+ {
19
+ super(pFable, pOptions, pServiceHash);
20
+ this.serviceType = 'PictProviderTimelineDragDrop';
21
+
22
+ // Set by parent view after construction
23
+ this._ParentTimeline = null;
24
+
25
+ // Current drag state
26
+ this._DragState =
27
+ {
28
+ Active: false,
29
+ SourceIndex: -1,
30
+ TargetIndex: -1,
31
+ InsertPosition: 'after' // 'before' | 'after'
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Called from ondragstart on a cut's drag handle.
37
+ */
38
+ onDragStart(pEvent, pCutIndex)
39
+ {
40
+ this._DragState.Active = true;
41
+ this._DragState.SourceIndex = pCutIndex;
42
+
43
+ if (pEvent && pEvent.dataTransfer)
44
+ {
45
+ pEvent.dataTransfer.effectAllowed = 'move';
46
+ // Required for Firefox
47
+ pEvent.dataTransfer.setData('text/plain', String(pCutIndex));
48
+ }
49
+
50
+ if (pEvent && pEvent.currentTarget)
51
+ {
52
+ pEvent.currentTarget.classList.add('pet-dragging');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Called from ondragover on each cut row. Detects whether the
58
+ * cursor is in the top or bottom half to determine insert
59
+ * position.
60
+ */
61
+ onDragOver(pEvent, pCutIndex)
62
+ {
63
+ if (!this._DragState.Active)
64
+ {
65
+ return;
66
+ }
67
+
68
+ if (pEvent)
69
+ {
70
+ pEvent.preventDefault();
71
+ }
72
+
73
+ this._DragState.TargetIndex = pCutIndex;
74
+
75
+ // Detect top/bottom half for insert indicator
76
+ if (pEvent && pEvent.currentTarget)
77
+ {
78
+ let tmpRect = pEvent.currentTarget.getBoundingClientRect();
79
+ let tmpMidpoint = tmpRect.top + (tmpRect.height / 2);
80
+ this._DragState.InsertPosition = pEvent.clientY < tmpMidpoint ? 'before' : 'after';
81
+
82
+ // Visual feedback
83
+ pEvent.currentTarget.classList.remove('pet-drag-insert-before', 'pet-drag-insert-after');
84
+ pEvent.currentTarget.classList.add(
85
+ this._DragState.InsertPosition === 'before'
86
+ ? 'pet-drag-insert-before'
87
+ : 'pet-drag-insert-after');
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Called from ondragleave on each cut row.
93
+ */
94
+ onDragLeave(pEvent, pCutIndex)
95
+ {
96
+ if (pEvent && pEvent.currentTarget)
97
+ {
98
+ pEvent.currentTarget.classList.remove(
99
+ 'pet-drag-insert-before',
100
+ 'pet-drag-insert-after');
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Called from ondrop on each cut row.
106
+ */
107
+ onDrop(pEvent, pCutIndex)
108
+ {
109
+ if (pEvent)
110
+ {
111
+ pEvent.preventDefault();
112
+ }
113
+
114
+ if (!this._DragState.Active)
115
+ {
116
+ return;
117
+ }
118
+
119
+ let tmpFrom = this._DragState.SourceIndex;
120
+ let tmpTo = pCutIndex;
121
+
122
+ // Adjust for insert position
123
+ if (this._DragState.InsertPosition === 'after')
124
+ {
125
+ tmpTo = tmpTo + 1;
126
+ }
127
+
128
+ // Adjust if moving downward (source removal shifts indices)
129
+ if (tmpFrom < tmpTo)
130
+ {
131
+ tmpTo = tmpTo - 1;
132
+ }
133
+
134
+ if (this._ParentTimeline && this._ParentTimeline._TimelineOps)
135
+ {
136
+ this._ParentTimeline._TimelineOps.moveCut(tmpFrom, tmpTo);
137
+ }
138
+
139
+ this._resetDragState();
140
+
141
+ // Re-render the timeline
142
+ if (this._ParentTimeline)
143
+ {
144
+ this._ParentTimeline.render();
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Called from ondragend on the drag handle (fires even if drop
150
+ * was cancelled).
151
+ */
152
+ onDragEnd(pEvent)
153
+ {
154
+ this._resetDragState();
155
+
156
+ // Clean up any lingering CSS classes
157
+ if (typeof document !== 'undefined')
158
+ {
159
+ let tmpEls = document.querySelectorAll('.pet-dragging, .pet-drag-insert-before, .pet-drag-insert-after');
160
+ for (let i = 0; i < tmpEls.length; i++)
161
+ {
162
+ tmpEls[i].classList.remove('pet-dragging', 'pet-drag-insert-before', 'pet-drag-insert-after');
163
+ }
164
+ }
165
+ }
166
+
167
+ _resetDragState()
168
+ {
169
+ this._DragState.Active = false;
170
+ this._DragState.SourceIndex = -1;
171
+ this._DragState.TargetIndex = -1;
172
+ this._DragState.InsertPosition = 'after';
173
+ }
174
+ }
175
+
176
+ module.exports = PictProviderTimelineDragDrop;
177
+ module.exports.default_configuration = {};