Make the first run legible, and join the two halves - #7
Open
pallaoro wants to merge 8 commits into
Open
Conversation
The token layer had drifted from the current design system: a slate-gray palette where the neutrals are meant to be warm, coral as the primary button where the primary is ink, 6/8/12px corners where the scale is 8/12/16/24, and no accent, info or category families at all. Everything downstream inherited that, so this replaces the tokens with the generated values and then follows them through both routes. - Tokens: the full generated palette (neutrals, accent, four statuses, four track categories) in light and dark, the type scale as `--text-*` so a component names a step instead of a pixel size, and the three elevation recipes as utilities. The edge is an inset ring everywhere, never a painted border; in dark mode the drops disappear and the ring is the whole treatment. - One ink action per screen. Coral goes back to being the accent, which now appears only on the app icon and the focus ring. Auto-cut, Clean up clip and New edit become secondary; Export and New video keep the solid. - Both timelines share one category palette, so a video clip is the same hue in the composition timeline and the footage timeline. Clips are a tint fill with a solid bar at the left edge rather than a solid block: a timeline is mostly one kind of clip, and a wall of the solid role reads as a paint chart. - window.confirm and alert are gone. Deleting a composition or a library asset opens a dialog that names the object and says what is lost; a failed render reports in place. An agent driving the browser could not answer a native dialog at all. - Uppercase tracked labels are gone from the inspectors. That style is the micro label above a KPI number; section titles and field labels are sentence case in `label`. - Empty states say what the thing is and offer the way forward, and sit on the pane rather than inside a card. Loading renders the shape of the answer. - Phones and tablets get one pane at a time. Below lg the footage editor shows Library, Edit or Options, chosen from a second toolbar row; the four-region grid stays a desktop layout. Inputs hold 16px so iOS does not zoom. - The starter composition moves to its own module: those colours are pixels inside the user's video, not app chrome, so the design tokens do not apply and the lint exempts that one file. - Inter is actually loaded now, and the app icon carries the accent hue. scripts/apps-design-lint.py goes from 33 findings to clean. Typecheck and build pass, and both routes plus the delete dialog were driven in a browser at 390, 1024 and 1920.
`.field` was unlayered, and unlayered CSS outranks everything Tailwind emits. Its `width: 100%` therefore beat `w-8` and blew the inspector's 32px colour swatch up to the full 466px panel width, squeezing the hex input beside it to 24px and pushing a field past the right edge of the viewport. Moving the rules into @layer base makes the recipe a default that any utility on the element overrides, which is the precedence a component author expects. Also drops a local `.tabular-nums` that duplicated Tailwind's own utility. Measured in the browser: swatch 32×32, its value input 426px, nothing overflowing.
Compositions animate in with gsap.from({opacity: 0}), so at t=0 every element
is still invisible. Both the gallery thumbnails and the editor sat at t=0, so a
new video showed a black rectangle: the starter composition that is meant to
teach you what this app is was showing nothing, and the app read as broken on
first open.
Seek to the halfway frame instead, which is what a video tool would pick for a
poster: past the entrances, before any outro. The preview harness already
accepted ?seek=, so this is a parameter, not new machinery.
Three changes to the first run, plus the defect they exposed.
- Open a composition with the headline clip already selected, so the first
thing on screen is a field holding the text you are about to change. An
editor that opens on "select something to edit it" spends the user's first
move on housekeeping.
- Name a new composition after what is on screen ("Product launch title card").
Three things called "Untitled" tell you nothing.
- Say which section is which, in the same shape for both: "Graphics you make
from scratch" against "Video you already shot". When a section is empty its
empty state carries the action, so the header does not duplicate the button
and the page keeps one solid action.
The defect: the preview iframe renders the SAVED composition, because the
harness is served by /api/compositions/:id/preview. Reloading it after an
inspector edit without saving first just re-showed the old frame, so you typed
your title and the canvas never changed. The save button lives only in the
Compose tab, so from the default Timeline tab there was no way to make your own
edit appear at all. The debounced refresh now persists first, then reloads.
The composition side and the footage side were two unconnected documents: a render only ever produced a row in render_jobs and an MP4 you could download. You could not put your own title card into your own cut, which is most of what a launch video is. A finished composition IS footage, so it now becomes a media-library asset: - render_jobs.asset_id records what the render produced - the render handler inserts the assets row, with the duration computed from the composition's own clip timings (the renderer produces exactly that, and the footage timeline needs a length synchronously to lay the clip out) - a completed render offers "Use in an edit", which starts a cut with that clip at the head of the main track Export staging already streams from this app's own storage, so nothing in the export path changed. This exposed a latent bug it now trips reliably: useSourceDurations gave srcDur an empty dep array, so when a duration landed in the cache the segments memo never recomputed. It only bit when the EDL already referenced an asset at mount, which is exactly what "Use in an edit" creates: every clip rendered at zero length. srcDur now takes the version counter, so a resolved duration invalidates the memo. Verified in the browser with a real clip: the cut opens at 0:04.2 with the filmstrip on the main track. NOT verified: the render service call itself, which needs CLAWNIFY_TOKEN and is unchanged by this commit.
The stage was sized by `aspect-ratio` off a full-width box with `max-height: 100%`. The parent's height is indefinite, so max-height never applied: at 1920x936 the stage computed to 1392x783 inside a 560px-tall pane and the bottom of the frame was clipped, taking the caption safe area with it. The ratio was right; the size was not. Measure the pane and take the limiting dimension, which is what the composition preview's harness already does (`min(innerWidth/w, innerHeight/h)`). Measured on the CONTENT box, because the pane carries padding and measuring through it puts the stage back over the edge the padding exists to clear. Verified at 1920x936, 1600x1000 and 1280x620: exact 16:9, inside the padding on all four sides, nothing clipped.
# Conflicts: # src/client/app.tsx # src/client/edit.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows the design-system pass (#6). That pass made the app look right; opening it still told you nothing about what to do, and the two halves were unconnected.
The first run
A new video showed a black rectangle. Compositions animate in with
gsap.from({opacity: 0}), so att=0every element is invisible. Both the gallery thumbnails and the editor sat att=0, so the starter composition that is meant to teach you what this app is was showing nothing, and the app read as broken on open. Both now seek to the halfway frame, which is what a video tool picks for a poster. The preview harness already accepted?seek=, so this is a parameter, not new machinery.The editor opens with the headline selected, so the first thing on screen is a field holding the text you are about to change. An editor that opens on "select something to edit it" spends the user's first move on housekeeping.
That exposed a defect: the preview iframe renders the saved composition, because the harness is served by
/api/compositions/:id/preview. Reloading it after an inspector edit without saving first just re-showed the old frame, so you typed your title and the canvas never changed — and the save button lives only in the Compose tab, so from the default Timeline tab there was no way to make your own edit appear at all. The debounced refresh now persists, then reloads.Copy: a new composition is named after what is on screen ("Product launch title card") rather than "Untitled", and each section says which one you want — "Graphics you make from scratch" against "Video you already shot". When a section is empty its empty state carries the action, so the page keeps one solid button.
The seam
The composition side and the footage side were two unconnected documents. A render only ever produced a row in
render_jobsand an MP4 you could download; you could not put your own title card into your own cut, which is most of what a launch video is.A finished composition is footage, so it now becomes a media-library asset:
render_jobs.asset_idrecords what the render produced, the render handler inserts theassetsrow with the duration computed from the composition's own clip timings, and a completed render offers Use in an edit, which starts a cut with that clip at the head of the main track. Export staging already streams from this app's storage, so the export path is unchanged.That exposed a latent bug it now trips reliably:
useSourceDurationsgavesrcDuran empty dep array, so when a duration landed in the cache thesegmentsmemo never recomputed. It only bit when the EDL already referenced an asset at mount, which is exactly what "Use in an edit" creates: every clip rendered at zero length.The player
The footage stage was sized by
aspect-ratiooff a full-width box withmax-height: 100%. The parent's height is indefinite, so max-height never applied: at 1920x936 the stage computed to 1392x783 inside a 560px-tall pane and the bottom of the frame was clipped, taking the caption safe area with it. The ratio was right; the size was not. It now measures the pane and takes the limiting dimension, which is what the composition preview's harness already does.Verification
Typecheck,
scripts/apps-design-lint.pyand build all pass. Driven in a browser: a new video from the empty state through to a first edit landing on the canvas; "Use in an edit" producing a cut that opens at0:04.2with the filmstrip on the main track; the player at 1920x936, 1600x1000 and 1280x620 with an exact 16:9 inside the padding on all four sides.Not verified: the render service call itself. It needs
CLAWNIFY_TOKEN, soPOST /api/rendersreturns 503 locally. The seam was exercised with a real uploaded clip standing in for a render. Theassetsinsert inside the render handler has never executed.Note on scope
This makes a launch video assemblable; it does not make the composition side a builder. You still cannot add an element to a composition without writing HTML. That is tracked separately.