retake-dev 0.4.0 → 0.5.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.
@@ -54,11 +54,40 @@ body.dragging #wb-stage, body.scrubbing #wb-stage { pointer-events: none; }
54
54
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 36px), var(--glass);
55
55
  -webkit-backdrop-filter: blur(28px) saturate(160%); backdrop-filter: blur(28px) saturate(160%);
56
56
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10), 0 -8px 32px rgba(0, 0, 0, 0.35); }
57
+ /* Folded away (the divider dragged to the bottom edge, or ⌥T): it slides down
58
+ out of sight and the round button in the corner brings it back. */
59
+ #wb-dock { padding-bottom: env(safe-area-inset-bottom, 0px);
60
+ transition: transform .24s var(--ease), opacity .2s var(--ease), visibility 0s linear 0s; }
61
+ #wb-dock.will-collapse { opacity: .55; }
62
+ /* Mid-drag it follows the pointer exactly (translateY below the minimum height,
63
+ see 10-dock.js); let go and it eases from there into the icon or back up. */
64
+ body.dragging #wb-dock { transition: opacity .12s var(--ease); will-change: transform; }
65
+ body.collapsed #wb-dock { transform: translateY(calc(100% + 24px)); opacity: 0; visibility: hidden; pointer-events: none;
66
+ transition: transform .24s var(--ease), opacity .2s var(--ease), visibility 0s linear .24s; }
67
+ /* Placed by 10-dock.js (left/top: dragged anywhere, snapped to a side edge). */
68
+ #wb-fab { position: fixed; z-index: 10; left: calc(100vw - 60px); top: calc(100vh - 60px); touch-action: none; cursor: grab;
69
+ width: 44px; height: 44px; border-radius: 999px; color: var(--blue);
70
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)), var(--glass);
71
+ -webkit-backdrop-filter: blur(28px) saturate(160%); backdrop-filter: blur(28px) saturate(160%);
72
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10), inset 0 0 0 1px var(--hair), var(--shadow); animation: fab-in .22s var(--ease); }
73
+ #wb-fab[hidden] { display: none; }
74
+ #wb-fab.moving { cursor: grabbing; transform: scale(1.06); transition: transform .12s var(--ease); }
75
+ #wb-fab.snapping { transition: left .28s var(--ease), top .28s var(--ease), transform .12s var(--ease); }
76
+ @media (hover: hover) { #wb-fab:hover { background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02)), var(--glass); } }
77
+ #wb-fab svg { display: block; }
78
+ /* The phase, as the readout shows it: green live, blue playing, amber paused. */
79
+ #wb-fab .dot { position: absolute; top: 9px; right: 9px; width: 6px; height: 6px; border-radius: 999px; background: var(--dim); box-shadow: 0 0 0 2px rgba(22, 22, 26, 0.9); }
80
+ #wb-fab[data-phase="live"] .dot { background: var(--green); }
81
+ #wb-fab[data-phase="playing"] .dot { background: var(--blue); }
82
+ #wb-fab[data-phase="paused"] .dot, #wb-fab[data-phase="scrubbing"] .dot { background: var(--amber); }
83
+ @keyframes fab-in { from { opacity: 0; transform: scale(.6) translateY(8px); } }
57
84
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
85
+ #wb-fab { background: rgba(20, 20, 24, 0.92); }
58
86
  #wb-dock { background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 36px), rgba(20, 20, 24, 0.92); }
59
87
  }
60
88
  /* The grab handle at the top centre: always there, brighter on hover. */
61
- .divider { position: absolute; left: 0; right: 0; top: 0; height: 12px; cursor: ns-resize; z-index: 3; display: flex; justify-content: center; }
89
+ /* touch-action: a finger drag on it is the dock's, never the page's scroll. */
90
+ .divider { position: absolute; left: 0; right: 0; top: 0; height: 12px; cursor: ns-resize; z-index: 3; display: flex; justify-content: center; touch-action: none; }
62
91
  .divider span { margin-top: 4px; width: 36px; height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.18); transition: background .15s var(--ease), width .15s var(--ease); }
63
92
  .divider:hover span, body.dragging .divider span { background: rgba(255, 255, 255, 0.45); width: 44px; }
64
93
 
@@ -71,12 +100,11 @@ button:active { transform: scale(0.96); }
71
100
  .head { position: relative; flex: none; height: 48px; margin-top: 8px; display: flex; align-items: center; gap: 8px; padding: 0 16px; }
72
101
  .left, .right { display: flex; align-items: center; gap: 8px; flex: none; }
73
102
  .right { margin-left: auto; }
74
- /* Phone width: the tools scroll sideways rather than run off the edge. */
75
- @media (max-width: 520px) { .head { padding: 0 10px; gap: 6px; } .readout .phase { display: none; } .right { flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; } .right::-webkit-scrollbar { display: none; } }
76
103
  /* One button style for the text controls: 32px, full round, a ghost fill. */
77
104
  .btn { flex: none; height: 32px; padding: 0 14px; gap: 8px; border-radius: 999px; background: var(--fill); color: var(--muted);
78
105
  font: 500 13px/1 var(--sans); box-shadow: inset 0 0 0 1px var(--hair); }
79
106
  @media (hover: hover) { .btn:hover { color: var(--white); background: var(--fill-2); } }
107
+ .btn.fresh svg { display: none; }
80
108
 
81
109
  /* Transport: play and the clock in one capsule. */
82
110
  .transport { display: flex; align-items: center; gap: 10px; height: 36px; padding: 0 14px 0 4px; border-radius: 999px;
@@ -118,6 +146,29 @@ button:active { transform: scale(0.96); }
118
146
  box-shadow: 0 0 0 2px rgba(22, 22, 26, 0.9); transform: scale(0); opacity: 0; transition: transform .2s var(--ease), opacity .2s var(--ease); }
119
147
  .notes-count.has .n { transform: none; opacity: 1; }
120
148
 
149
+ /* Phone width: the phase word goes, the row tightens, and Start fresh becomes
150
+ its icon; anything still too wide scrolls sideways rather than run off the edge. */
151
+ @media (max-width: 520px) { .head { padding: 0 10px; gap: 6px; } .readout .phase { display: none; } .right { gap: 6px; flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; } .right::-webkit-scrollbar { display: none; } }
152
+ @media (max-width: 440px) { .btn.fresh { width: 32px; padding: 0; } .btn.fresh span { display: none; } .btn.fresh svg { display: block; } }
153
+ /* Fingers (and phone widths): 40px targets. The handle's strip reaches 16px
154
+ above the dock; lane names get a narrower gutter so the track keeps its
155
+ width; the glass is denser, so the page under it doesn't muddy the dock. */
156
+ @media (pointer: coarse), (max-width: 520px) {
157
+ .divider { top: -16px; height: 32px; }
158
+ .divider span { margin-top: 20px; }
159
+ .head { height: 56px; }
160
+ .transport { height: 44px; gap: 8px; padding: 0 12px 0 2px; }
161
+ .play { width: 40px; height: 40px; }
162
+ .tools { height: 44px; padding: 2px; }
163
+ .tool { width: 40px; height: 40px; }
164
+ .btn { height: 40px; }
165
+ .notes-count { width: 32px; }
166
+ }
167
+ @media (max-width: 440px) { .btn.fresh { width: 40px; } }
168
+ @media (max-width: 520px) {
169
+ :root { --glass: rgba(20, 20, 24, 0.9); }
170
+ .gutter { width: 76px; }
171
+ }
121
172
  /* The one hint: a small pill. */
122
173
  /* Warnings only, centred on the dock. */
123
174
  .hint { position: absolute; left: 50%; top: 12px; transform: translateX(-50%); height: 24px; padding: 0 12px; display: inline-flex; align-items: center; border-radius: 999px;
@@ -163,10 +214,6 @@ body.scrubbing, body.scrubbing .track { cursor: ew-resize; }
163
214
  body.branching .track, body.branching .track * { cursor: crosshair !important; }
164
215
 
165
216
  /* Following was stopped by hand: this brings it back. */
166
- .live-btn { position: absolute; right: 8px; top: 24px; z-index: 2; height: 24px; padding: 0 10px; border-radius: 999px;
167
- background: var(--fill-2); color: var(--white); font: 500 12px/1 var(--sans); }
168
- @media (hover: hover) { .live-btn:hover { background: rgba(255, 255, 255, 0.18); } }
169
- .live-btn[hidden] { display: none; }
170
217
 
171
218
  /* ruler: numbers are monospace */
172
219
  .lines .rule { stroke: var(--hair); stroke-width: 1; }
@@ -230,6 +277,7 @@ body.branching .track, body.branching .track * { cursor: crosshair !important; }
230
277
  #wb-layers .layer.pseudo { padding-left: 14px; }
231
278
  #wb-layers .layer.on { color: var(--ink); background: var(--amber); }
232
279
  #wb-layers .more { padding: 2px 6px; color: var(--dim); }
280
+ #wb-layers .layer.skipped:not(.on) { color: var(--dim); }
233
281
  #wb-hl.select::after { background: var(--white); }
234
282
  #wb-scope { position: fixed; pointer-events: none; border: 1px dashed var(--muted); border-radius: 8px; z-index: 14; display: none; }
235
283
  #wb-pins { position: fixed; inset: 0; pointer-events: none; z-index: 15; }
@@ -237,6 +285,7 @@ body.branching .track, body.branching .track * { cursor: crosshair !important; }
237
285
  color: var(--ink); font: 600 11px/1 var(--mono); box-shadow: 0 0 0 2px rgba(22, 22, 26, 0.85), var(--shadow); transition: transform .12s var(--ease), opacity .15s var(--ease); }
238
286
  @media (hover: hover) { .canvas-pin:hover { transform: scale(1.12); } }
239
287
  .canvas-pin.away { opacity: .45; }
288
+ body.picking-notes .canvas-pin { pointer-events: none; }
240
289
  @media (hover: hover) { .canvas-pin.away:hover { opacity: 1; } }
241
290
 
242
291
  /* ---- popovers: a note, a context menu ---- */
@@ -292,4 +341,26 @@ body.branching .track, body.branching .track * { cursor: crosshair !important; }
292
341
  .ctx button.danger { color: var(--red); }
293
342
  .ctx .menu-note { padding: 8px; color: var(--dim); font-size: 12px; line-height: 16px; }
294
343
 
295
- @media (prefers-reduced-motion: reduce) { * { animation-duration: 0s !important; transition-duration: 0s !important; } }
344
+ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; } }
345
+
346
+ /* The focused element's own row (32-anims.js): its name in the gutter, where it is on its clock in the hint. */
347
+ .focus-name { position: absolute; left: 0; right: 0; padding: 0 8px; display: flex; align-items: center; font: 500 10px/1 var(--mono); color: #fde68a;
348
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
349
+ .gutter.dots .focus-name { padding: 0 2px; font-size: 0; }
350
+ .gutter.dots .focus-name::before { content: ""; width: 8px; height: 8px; margin: 0 auto; border-radius: 2px; background: #fde68a; transform: rotate(45deg); }
351
+ .hint.readout { max-width: max(0px, calc(100% - 420px)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 11px; display: block; line-height: 24px; }
352
+ @media (max-width: 760px) { .hint.readout { display: none; } }
353
+ .note-at { margin-top: 6px; font: 500 11px/15px var(--mono); color: #fde68a; }
354
+ .note-asked { all: unset; cursor: pointer; display: block; margin-top: 6px; padding: 3px 8px; border-radius: 6px; font: 400 11px/15px var(--sans); color: var(--muted);
355
+ background: rgba(253, 230, 138, 0.08); border: 1px solid rgba(253, 230, 138, 0.2); }
356
+ .note-asked[hidden] { display: none; }
357
+ @media (hover: hover) { .note-asked:hover { color: var(--fg); } }
358
+ /* Where the open clip moves the element, on the app: its start and end boxes, the path of its centre, the range in full colour. */
359
+ #wb-path { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 14; overflow: visible; }
360
+ #wb-path[hidden] { display: none; }
361
+ #wb-path .edge { fill: none; stroke: #fde68a; stroke-opacity: .55; stroke-width: 1; stroke-dasharray: 4 3; }
362
+ #wb-path .edge.on { stroke-opacity: 1; }
363
+ #wb-path .path { fill: none; stroke: #fde68a; stroke-opacity: .6; stroke-width: 1.5; stroke-dasharray: 1 3; stroke-linecap: round; }
364
+ #wb-path .path.on { stroke-opacity: 1; stroke-dasharray: none; stroke-width: 2; }
365
+ #wb-path .tick { fill: #fff; }
366
+ #wb-path text { font: 600 10px var(--mono); fill: #fde68a; }
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>Prototype</title>
7
7
  <style>/*CSS*/</style>
8
8
  </head>
@@ -35,21 +35,28 @@
35
35
  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/></svg>
36
36
  <span class="n">0</span>
37
37
  </span>
38
- <button class="btn fresh" data-a="fresh">Start fresh</button>
38
+ <button class="btn fresh" data-a="fresh" aria-label="Start fresh" title="Start fresh">
39
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
40
+ <span>Start fresh</span>
41
+ </button>
39
42
  </div>
40
43
  </div>
41
44
  <div class="body">
42
45
  <div class="gutter" aria-label="Timelines"></div>
43
46
  <div class="track">
44
47
  <canvas class="lines" aria-label="Timeline"></canvas>
45
- <button class="live-btn" data-a="live" hidden>Live →</button>
46
48
  </div>
47
49
  </div>
48
50
  </div>
51
+ <button id="wb-fab" data-a="expand" aria-label="Show timeline" title="Show timeline (⌥T)" hidden>
52
+ <!--MARK-->
53
+ <i class="dot" aria-hidden="true"></i>
54
+ </button>
49
55
  <div id="wb-tip" class="tip" hidden></div>
50
56
  <div id="wb-layers" hidden></div>
51
57
  <div id="wb-hl"></div>
52
58
  <div id="wb-scope"></div>
59
+ <svg id="wb-path" hidden aria-hidden="true"></svg>
53
60
  <div id="wb-pins"></div>
54
61
  <div id="wb-note" class="card" hidden></div>
55
62
  <div id="wb-menu" class="card ctx" hidden></div>
package/types/client.d.ts CHANGED
@@ -25,6 +25,13 @@ export interface RetakeClip {
25
25
  property?: string
26
26
  path: Array<string | number>
27
27
  iterations?: "infinite" | number
28
+ /** One iteration's duration (ms), for clips that repeat. */
29
+ dur?: number
30
+ /** Start delay (ms), included in `start`..`end`. */
31
+ delay?: number
32
+ /** First and last keyframe's values (up to 4 properties), e.g. `{ opacity: "0" }` → `{ opacity: "1" }`. */
33
+ from?: Record<string, string>
34
+ to?: Record<string, string>
28
35
  pseudoElement?: string
29
36
  }
30
37
 
@@ -69,5 +76,14 @@ export interface RetakeRuntime {
69
76
  declare global {
70
77
  interface Window {
71
78
  __retake?: RetakeRuntime | { readonly inert: true }
79
+ /**
80
+ * In the dock's frame: how many px the dock covers at the bottom of the
81
+ * page (0 when it's folded away). A `retake:dock` event on `window`
82
+ * (`detail: { height }`) says when it changes.
83
+ */
84
+ __retakeDockHeight?: number
85
+ }
86
+ interface WindowEventMap {
87
+ "retake:dock": CustomEvent<{ height: number }>
72
88
  }
73
89
  }
package/types/index.d.ts CHANGED
@@ -78,6 +78,15 @@ export interface ShellOptions {
78
78
  marker?: "url" | "header"
79
79
  /** Rebuilds ask the server for the page as it was recorded (front server only). */
80
80
  docs?: boolean
81
+ /**
82
+ * `false`: there's no Retake server behind the dock (a static or serverless
83
+ * deploy). It never requests `/__retake/` (no session, no events) and keeps
84
+ * the session in memory for the page. Default: a server is assumed, and a 404
85
+ * from it falls back to memory.
86
+ */
87
+ server?: boolean
88
+ /** The project folder: source paths in notes are written relative to it. */
89
+ root?: string
81
90
  /** The token mutating `/__retake/` requests carry. */
82
91
  token?: string
83
92
  }
@@ -85,6 +94,13 @@ export interface ShellOptions {
85
94
  /** The dock page: a complete HTML document with the app in a frame. */
86
95
  export function shellHtml(options?: ShellOptions): string
87
96
 
97
+ /**
98
+ * Retake's mark as an SVG string (24×24 viewBox, no width or height): a
99
+ * playhead on a track with an arc back to it. The arc and the played part are
100
+ * `currentColor`, the rest white, so it reads on a dark background.
101
+ */
102
+ export function markSvg(): string
103
+
88
104
  export interface RuntimeTagOptions {
89
105
  /** The runtime's setup. */
90
106
  rt?: RuntimeConfig
@@ -0,0 +1,50 @@
1
+ // Types for `retake-dev/next`: Retake from a Next.js app's middleware
2
+ // (src/next.js). Dev only: outside `next dev` every request goes straight on.
3
+
4
+ /**
5
+ * A middleware (Next 16's `proxy.ts`, Next 15's `middleware.ts`): in `next dev`
6
+ * a top-level page load gets the dock, the dock's frame gets the page with the
7
+ * time runtime first in `<head>`, and `/__retake/*` is the session API (kept
8
+ * in `.retake/`, read by `retake mcp`). Everything else goes on as it was.
9
+ *
10
+ * ```ts
11
+ * // proxy.ts (Next 16)
12
+ * export { default } from "retake-dev/next"
13
+ * ```
14
+ *
15
+ * ```ts
16
+ * // middleware.ts (Next 15: the Node.js runtime)
17
+ * import retake from "retake-dev/next"
18
+ * export default retake
19
+ * export const config = { runtime: "nodejs" }
20
+ * ```
21
+ *
22
+ * Next reads `config` from your file as written (it can't be re-exported).
23
+ * With a matcher of your own, add these entries to it:
24
+ *
25
+ * ```ts
26
+ * "/__retake/:path*",
27
+ * { source: "/((?!_next/static|_next/image).*)", has: [{ type: "header", key: "sec-fetch-mode", value: "navigate" }] },
28
+ * ```
29
+ */
30
+ declare const retake: (req: Request, event?: unknown) => Promise<Response | undefined>
31
+ export default retake
32
+
33
+ /**
34
+ * Wraps a middleware of your own: in `next dev` Retake answers page loads and
35
+ * `/__retake/*` first; every other request (and every request outside
36
+ * `next dev`) goes to yours. The dock's frame is fetched through your
37
+ * middleware too, so its redirects, rewrites and headers apply to the page.
38
+ *
39
+ * ```ts
40
+ * import { withRetake } from "retake-dev/next"
41
+ * export default withRetake(async (req) => { ... })
42
+ * ```
43
+ */
44
+ export function withRetake<M extends (req: any, event?: any) => any>(middleware?: M): (req: Request, event?: unknown) => Promise<Awaited<ReturnType<M>> | Response | undefined>
45
+
46
+ /**
47
+ * Retake's answer to a request, or `undefined` to let it through (always
48
+ * `undefined` outside `next dev`). For a middleware that decides itself.
49
+ */
50
+ export function retakeResponse(req: Request): Promise<Response | undefined>