retake-dev 0.4.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.
- package/LICENSE +91 -0
- package/README.md +198 -0
- package/bin/retake.js +319 -0
- package/package.json +87 -0
- package/src/code-versions.js +357 -0
- package/src/core.js +265 -0
- package/src/plugin.js +137 -0
- package/src/runtime/00-core.js +308 -0
- package/src/runtime/10-animations.js +362 -0
- package/src/runtime/20-media.js +157 -0
- package/src/runtime/30-recorder.js +315 -0
- package/src/runtime/32-state.js +315 -0
- package/src/runtime/35-network.js +789 -0
- package/src/runtime/36-scripts.js +129 -0
- package/src/runtime/37-next.js +150 -0
- package/src/runtime/38-observers.js +252 -0
- package/src/runtime/40-input.js +795 -0
- package/src/runtime/45-hover.js +78 -0
- package/src/runtime/50-engine.js +341 -0
- package/src/runtime/60-preview.js +437 -0
- package/src/runtime/65-timeline.js +289 -0
- package/src/runtime/66-activity.js +114 -0
- package/src/runtime/67-csssource.js +226 -0
- package/src/runtime/70-boot.js +460 -0
- package/src/server/api.js +285 -0
- package/src/server/child.js +174 -0
- package/src/server/detect.js +167 -0
- package/src/server/front.js +647 -0
- package/src/server/mcp.js +332 -0
- package/src/shell/00-state.js +85 -0
- package/src/shell/05-api.js +136 -0
- package/src/shell/10-dock.js +754 -0
- package/src/shell/12-checkpoint.js +121 -0
- package/src/shell/15-session.js +291 -0
- package/src/shell/20-timeline.js +734 -0
- package/src/shell/25-input.js +537 -0
- package/src/shell/30-notes.js +870 -0
- package/src/shell/40-code.js +80 -0
- package/src/shell/90-handle.js +15 -0
- package/src/shell/shell.css +295 -0
- package/src/shell/shell.html +59 -0
- package/types/client.d.ts +73 -0
- package/types/index.d.ts +111 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Code versions per timeline (with `--code-branches`): when the prototype's
|
|
2
|
+
// source changes, the timeline you're on takes the new code and the moment is
|
|
3
|
+
// rebuilt on it. Each timeline remembers its code; stepping into one checks its
|
|
4
|
+
// code back out, so timelines can differ in code as well as in what happened.
|
|
5
|
+
|
|
6
|
+
const CODE_BRANCHES = !!(window.__retakeConfig && window.__retakeConfig.codeBranches)
|
|
7
|
+
|
|
8
|
+
// Checks a version out. { ok } once the files on disk are that version;
|
|
9
|
+
// `left` is the snapshot the server took of the files just before (the real
|
|
10
|
+
// code of the timeline being left, edits of the last moments included).
|
|
11
|
+
async function checkoutCode(version) {
|
|
12
|
+
try {
|
|
13
|
+
const r = await api("POST", `checkout?v=${encodeURIComponent(version)}`)
|
|
14
|
+
if (r.missing) return { ok: false }
|
|
15
|
+
const v = r.value && typeof r.value === "object" ? r.value : {}
|
|
16
|
+
return { ok: v.ok !== false, left: v.left || null }
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.warn("[retake] couldn't check out that timeline's code", err)
|
|
19
|
+
return { ok: false }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// What the server last said about the code on disk.
|
|
24
|
+
const code = { newest: null }
|
|
25
|
+
async function codeState() {
|
|
26
|
+
try {
|
|
27
|
+
const v = await (await fetch("/__retake/version")).json()
|
|
28
|
+
return v && v.version ? v : null
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// After a checkout: the timeline we left gets the code it really had. The
|
|
35
|
+
// server says so (`left`); an older server doesn't, so a snapshot that
|
|
36
|
+
// appeared during the checkout and belongs to no timeline is taken as it.
|
|
37
|
+
async function settleLeftVersion(leaving, r) {
|
|
38
|
+
if (!leaving) return
|
|
39
|
+
if (r.left) {
|
|
40
|
+
leaving.version = r.left
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const v = await codeState()
|
|
44
|
+
if (v && v.newest && v.newest !== code.newest && !D.branches.some((b) => b.version === v.newest)) leaving.version = v.newest
|
|
45
|
+
if (v && v.newest) code.newest = v.newest
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (CODE_BRANCHES) {
|
|
49
|
+
setInterval(async () => {
|
|
50
|
+
if (D.switching || !D.PT || D.building || net.restoring) return
|
|
51
|
+
const v = await codeState()
|
|
52
|
+
if (!v || D.switching || !D.PT || D.building) return
|
|
53
|
+
const version = v.version
|
|
54
|
+
const cur = activeBranch()
|
|
55
|
+
// Code the server put back at startup isn't an edit made on this
|
|
56
|
+
// timeline: put this timeline's own code back instead of adopting it.
|
|
57
|
+
if (v.restored) {
|
|
58
|
+
if (cur.version && version !== cur.version) await checkoutCode(cur.version)
|
|
59
|
+
else if (!cur.version) cur.version = version
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (v.newest) code.newest = v.newest
|
|
63
|
+
if (!cur.version) {
|
|
64
|
+
cur.version = version
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (version === cur.version) return
|
|
68
|
+
// Not recording yet: just show the new code.
|
|
69
|
+
if (!D.PT.state().started) {
|
|
70
|
+
cur.version = version
|
|
71
|
+
return freshFrame()
|
|
72
|
+
}
|
|
73
|
+
// The code changed: the timeline you're on takes it (no new timeline;
|
|
74
|
+
// those only come from the +). Rebuild this moment on the new code.
|
|
75
|
+
const st = D.PT.state()
|
|
76
|
+
const t = st.previewing ? st.previewAt : st.now
|
|
77
|
+
cur.version = version
|
|
78
|
+
D.PT.load(JSON.stringify(D.PT.history()), t)
|
|
79
|
+
}, 500)
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A handle for tests and debugging. `state` is the live dock state object.
|
|
2
|
+
window.__retakeDock = {
|
|
3
|
+
state: D,
|
|
4
|
+
notes: () => D.notes,
|
|
5
|
+
prompt: (n) => prompt(n),
|
|
6
|
+
branches: () => D.branches.map((b) => ({ ...b, json: undefined, active: b.id === D.activeId })),
|
|
7
|
+
scope: () => D.scopeEl,
|
|
8
|
+
deleteTimeline: (id) => deleteTimeline(id),
|
|
9
|
+
switchTo: (id, t) => switchTo(id, t),
|
|
10
|
+
newTimelineAt: (t) => newTimelineAt(t),
|
|
11
|
+
fitAll: () => fitAll(),
|
|
12
|
+
checkpoint: () => (D.cp ? { at: D.cp.at, branchId: D.cp.branchId, ready: checkpointReady(D.cp) } : null),
|
|
13
|
+
goTo: (t, opts) => goTo(t, opts),
|
|
14
|
+
building: () => (D.building ? { id: D.building.id, target: D.building.target, via: D.building.via, play: !!D.building.play, fork: !!D.building.fork, visible: !!D.building.visible, ready: !!D.building.pt } : null),
|
|
15
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/* The dock: a glass notch rising from the bottom edge, 16px in from the sides. Sans
|
|
2
|
+
for words, monospace only for numbers. Each timeline has its own colour;
|
|
3
|
+
blue dots are things you did, amber is notes. Spacing on a 4/8 grid, 32px
|
|
4
|
+
controls, every transition under 300ms. */
|
|
5
|
+
:root {
|
|
6
|
+
--page: #0e0e11;
|
|
7
|
+
--glass: rgba(20, 20, 24, 0.74);
|
|
8
|
+
--glass-strong: rgba(26, 26, 31, 0.94);
|
|
9
|
+
--hair: rgba(255, 255, 255, 0.08);
|
|
10
|
+
--hair-2: rgba(255, 255, 255, 0.14);
|
|
11
|
+
--fill: rgba(255, 255, 255, 0.06);
|
|
12
|
+
--fill-2: rgba(255, 255, 255, 0.1);
|
|
13
|
+
--fg: #f2f2f5;
|
|
14
|
+
--white: #fff;
|
|
15
|
+
--muted: rgba(255, 255, 255, 0.62);
|
|
16
|
+
--dim: rgba(255, 255, 255, 0.4);
|
|
17
|
+
--faint: rgba(255, 255, 255, 0.16);
|
|
18
|
+
--ink: #16161a; /* text on light fills */
|
|
19
|
+
--amber: #ffb224;
|
|
20
|
+
--blue: #60a5fa;
|
|
21
|
+
--green: #4cc38a;
|
|
22
|
+
--red: #ff6b6b;
|
|
23
|
+
--mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
|
|
24
|
+
--sans: Geist, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
25
|
+
--ease: cubic-bezier(.2, .8, .2, 1);
|
|
26
|
+
--shadow: 0 16px 48px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
27
|
+
color-scheme: dark;
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
html, body { height: 100%; margin: 0; background: transparent; overflow: hidden; }
|
|
31
|
+
body { font: 400 13px/16px var(--sans); color: var(--fg); -webkit-font-smoothing: antialiased; }
|
|
32
|
+
|
|
33
|
+
/* ---- stage: the prototype fills the whole window; the notch floats over its
|
|
34
|
+
bottom edge, so the app's size never changes with the dock's. ---- */
|
|
35
|
+
#wb-stage { position: fixed; inset: 0; overflow: hidden; background: transparent; }
|
|
36
|
+
#wb-stage iframe { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border: 0; display: block; background: transparent; }
|
|
37
|
+
/* Frames being built behind the visible one are never seen (an app with no
|
|
38
|
+
background of its own would show through), but stay rendered: their replay
|
|
39
|
+
waits on animation frames. Not visibility:hidden (that would make them unfocusable). */
|
|
40
|
+
#wb-stage iframe.building, #wb-stage iframe.checkpoint { z-index: 0; opacity: 0; pointer-events: none; }
|
|
41
|
+
#wb-stage iframe.live { z-index: 1; }
|
|
42
|
+
/* A swap on a page with a canvas: the real moment fades in over the preview. */
|
|
43
|
+
#wb-stage iframe.leaving { z-index: 0; pointer-events: none; }
|
|
44
|
+
#wb-stage iframe.live[data-enter] { animation: wb-enter 120ms ease-out; }
|
|
45
|
+
@keyframes wb-enter { from { opacity: 0; } }
|
|
46
|
+
/* In the past the app is view-only, like a paused video. */
|
|
47
|
+
#wb-shield { position: absolute; inset: 0; z-index: 2; cursor: default; pointer-events: none; }
|
|
48
|
+
#wb-shield[hidden] { display: none; }
|
|
49
|
+
body.dragging #wb-stage, body.scrubbing #wb-stage { pointer-events: none; }
|
|
50
|
+
|
|
51
|
+
/* ---- the notch: glass rising from the bottom edge ---- */
|
|
52
|
+
#wb-dock { position: fixed; left: 16px; right: 16px; bottom: 0; z-index: 10; height: 200px; display: flex; flex-direction: column;
|
|
53
|
+
border-radius: 18px 18px 0 0; border: 0;
|
|
54
|
+
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 36px), var(--glass);
|
|
55
|
+
-webkit-backdrop-filter: blur(28px) saturate(160%); backdrop-filter: blur(28px) saturate(160%);
|
|
56
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10), 0 -8px 32px rgba(0, 0, 0, 0.35); }
|
|
57
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
58
|
+
#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
|
+
}
|
|
60
|
+
/* 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; }
|
|
62
|
+
.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
|
+
.divider:hover span, body.dragging .divider span { background: rgba(255, 255, 255, 0.45); width: 44px; }
|
|
64
|
+
|
|
65
|
+
button { all: unset; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: var(--muted);
|
|
66
|
+
transition: color .15s var(--ease), background-color .15s var(--ease), border-color .15s var(--ease), transform .1s var(--ease); }
|
|
67
|
+
button:focus-visible { outline: 2px solid rgba(96, 165, 250, 0.6); outline-offset: 2px; }
|
|
68
|
+
button:active { transform: scale(0.96); }
|
|
69
|
+
|
|
70
|
+
/* One 48px row, everything on one centre line; 16px sides like the ruler. */
|
|
71
|
+
.head { position: relative; flex: none; height: 48px; margin-top: 8px; display: flex; align-items: center; gap: 8px; padding: 0 16px; }
|
|
72
|
+
.left, .right { display: flex; align-items: center; gap: 8px; flex: none; }
|
|
73
|
+
.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
|
+
/* One button style for the text controls: 32px, full round, a ghost fill. */
|
|
77
|
+
.btn { flex: none; height: 32px; padding: 0 14px; gap: 8px; border-radius: 999px; background: var(--fill); color: var(--muted);
|
|
78
|
+
font: 500 13px/1 var(--sans); box-shadow: inset 0 0 0 1px var(--hair); }
|
|
79
|
+
@media (hover: hover) { .btn:hover { color: var(--white); background: var(--fill-2); } }
|
|
80
|
+
|
|
81
|
+
/* Transport: play and the clock in one capsule. */
|
|
82
|
+
.transport { display: flex; align-items: center; gap: 10px; height: 36px; padding: 0 14px 0 4px; border-radius: 999px;
|
|
83
|
+
background: rgba(255, 255, 255, 0.045); box-shadow: inset 0 0 0 1px var(--hair), inset 0 1px 0 rgba(255, 255, 255, 0.05); }
|
|
84
|
+
.play { flex: none; width: 28px; height: 28px; border-radius: 999px; color: var(--ink);
|
|
85
|
+
background: linear-gradient(to bottom, #fff, #dcdce2); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), inset 0 -1px 0 rgba(0, 0, 0, 0.08); }
|
|
86
|
+
@media (hover: hover) { .play:hover { background: linear-gradient(to bottom, #fff, #ececf0); } }
|
|
87
|
+
.play svg { display: block; }
|
|
88
|
+
.play .i-play { margin-left: 2px; }
|
|
89
|
+
.play .i-pause { display: none; }
|
|
90
|
+
.play.playing .i-play { display: none; }
|
|
91
|
+
.play.playing .i-pause { display: block; }
|
|
92
|
+
|
|
93
|
+
.readout { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; }
|
|
94
|
+
.readout .t { display: inline-block; min-width: 8ch; font: 500 13px/1 var(--mono); color: var(--fg); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
|
|
95
|
+
.readout .t .cs { color: var(--dim); }
|
|
96
|
+
.readout .phase { display: inline-flex; align-items: center; gap: 6px; padding-left: 10px; border-left: 1px solid var(--hair);
|
|
97
|
+
font: 500 12px/1 var(--sans); color: var(--muted); }
|
|
98
|
+
.readout .phase::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: var(--dim); }
|
|
99
|
+
.readout .phase[data-phase="live"] { color: var(--fg); }
|
|
100
|
+
.readout .phase[data-phase="live"]::before { background: var(--green); box-shadow: 0 0 0 3px rgba(76, 195, 138, 0.18); animation: live-pulse 2s var(--ease) infinite; }
|
|
101
|
+
.readout .phase[data-phase="playing"]::before { background: var(--blue); box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.18); }
|
|
102
|
+
.readout .phase[data-phase="paused"]::before, .readout .phase[data-phase="scrubbing"]::before { background: var(--amber); box-shadow: 0 0 0 3px rgba(255, 178, 36, 0.16); }
|
|
103
|
+
@keyframes live-pulse { 50% { box-shadow: 0 0 0 5px rgba(76, 195, 138, 0); } }
|
|
104
|
+
|
|
105
|
+
/* Tools: Hand and Comment as one segmented capsule. */
|
|
106
|
+
.tools { display: flex; gap: 2px; height: 36px; padding: 3px; border-radius: 999px; background: rgba(255, 255, 255, 0.045); box-shadow: inset 0 0 0 1px var(--hair); }
|
|
107
|
+
.tool { width: 30px; height: 30px; border-radius: 999px; color: var(--muted); }
|
|
108
|
+
@media (hover: hover) { .tool:hover { color: var(--white); } }
|
|
109
|
+
.tool.on { color: var(--white); background: var(--fill-2); box-shadow: inset 0 0 0 1px var(--hair-2), 0 1px 2px rgba(0, 0, 0, 0.3); }
|
|
110
|
+
.tool.disabled { opacity: .35; cursor: default; }
|
|
111
|
+
@media (hover: hover) { .tool.disabled:hover { color: var(--muted); } }
|
|
112
|
+
|
|
113
|
+
/* Notes: just the count, as a badge on the icon. */
|
|
114
|
+
.notes-count { position: relative; flex: none; display: inline-grid; place-items: center; width: 36px; height: 36px; color: var(--dim); }
|
|
115
|
+
.notes-count.has { color: var(--muted); }
|
|
116
|
+
.notes-count .n { position: absolute; top: 2px; right: 0; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; box-sizing: border-box;
|
|
117
|
+
display: grid; place-items: center; font: 600 10px/1 var(--mono); color: var(--ink); background: var(--amber);
|
|
118
|
+
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
|
+
.notes-count.has .n { transform: none; opacity: 1; }
|
|
120
|
+
|
|
121
|
+
/* The one hint: a small pill. */
|
|
122
|
+
/* Warnings only, centred on the dock. */
|
|
123
|
+
.hint { position: absolute; left: 50%; top: 12px; transform: translateX(-50%); height: 24px; padding: 0 12px; display: inline-flex; align-items: center; border-radius: 999px;
|
|
124
|
+
font: 500 12px/1 var(--sans); color: var(--muted); background: var(--fill); border: 1px solid var(--hair); white-space: nowrap;
|
|
125
|
+
overflow: hidden; text-overflow: ellipsis; min-width: 0; opacity: 0; transform: translate(-50%, 2px);
|
|
126
|
+
transition: opacity .2s var(--ease), transform .2s var(--ease); }
|
|
127
|
+
.hint:empty { visibility: hidden; }
|
|
128
|
+
.hint.show { opacity: 1; transform: translateX(-50%); }
|
|
129
|
+
.hint.warn { color: var(--red); border-color: rgba(255, 107, 107, 0.35); }
|
|
130
|
+
|
|
131
|
+
/* Start fresh's safety net: a toast where the hint sits. */
|
|
132
|
+
#wb-toast { position: absolute; left: 50%; top: 16px; transform: translateX(-50%); z-index: 4; height: 32px; padding: 0 4px 0 12px;
|
|
133
|
+
display: inline-flex; align-items: center; gap: 8px; border-radius: 999px; background: var(--glass-strong); border: 1px solid var(--hair-2);
|
|
134
|
+
box-shadow: var(--shadow); font: 500 12px/1 var(--sans); color: var(--fg); animation: card-in .16s var(--ease); }
|
|
135
|
+
#wb-toast[hidden] { display: none; }
|
|
136
|
+
#wb-toast button { height: 24px; padding: 0 10px; border-radius: 999px; background: var(--fill-2); color: var(--white); font: 600 12px/1 var(--sans); }
|
|
137
|
+
@media (hover: hover) { #wb-toast button:hover { background: rgba(255, 255, 255, 0.18); } }
|
|
138
|
+
.head:has(#wb-toast:not([hidden])) .hint { visibility: hidden; }
|
|
139
|
+
|
|
140
|
+
/* ---- timeline: lane names + the drawing ---- */
|
|
141
|
+
.body { position: relative; flex: 1; min-height: 0; display: flex; padding: 0 16px; }
|
|
142
|
+
.gutter { position: relative; flex: none; width: 120px; overflow: hidden; }
|
|
143
|
+
/* A label: its colour's dot and a short name, on its row; the active row
|
|
144
|
+
(and the hovered one) have a faint band, which carries on across the track. */
|
|
145
|
+
.lane-name { position: absolute; left: 0; right: 0; padding: 0 8px; display: flex; align-items: center; gap: 8px;
|
|
146
|
+
font: 500 12px/1 var(--sans); color: rgba(255, 255, 255, 0.55); white-space: nowrap; cursor: pointer; user-select: none;
|
|
147
|
+
background: transparent; transition: color .15s var(--ease), background-color .15s var(--ease), top .2s var(--ease); }
|
|
148
|
+
.lane-name i { flex: none; width: 8px; height: 8px; border-radius: 999px; background: var(--c); opacity: .55; transition: opacity .15s var(--ease); }
|
|
149
|
+
.lane-name span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
150
|
+
.lane-name.active { color: var(--white); background: rgba(255, 255, 255, 0.03); }
|
|
151
|
+
.lane-name.active i { opacity: 1; }
|
|
152
|
+
@media (hover: hover) { .lane-name:not(.active):hover { color: var(--fg); background: rgba(255, 255, 255, 0.02); } .lane-name:not(.active):hover i { opacity: .8; } }
|
|
153
|
+
/* A short dock: just the colour dots, the name on hover. */
|
|
154
|
+
.gutter.dots { width: 28px; }
|
|
155
|
+
.gutter.dots .lane-name { padding: 0; justify-content: center; }
|
|
156
|
+
.gutter.dots .lane-name span { display: none; }
|
|
157
|
+
.gutter.dots .lane-name.active i { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.25); }
|
|
158
|
+
.lane-name input { all: unset; width: 100%; font: inherit; color: var(--white); border-bottom: 1px solid var(--dim); }
|
|
159
|
+
|
|
160
|
+
.track { position: relative; flex: 1; min-width: 80px; overflow: hidden; cursor: default; touch-action: none; }
|
|
161
|
+
.lines { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; user-select: none; }
|
|
162
|
+
body.scrubbing, body.scrubbing .track { cursor: ew-resize; }
|
|
163
|
+
body.branching .track, body.branching .track * { cursor: crosshair !important; }
|
|
164
|
+
|
|
165
|
+
/* 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
|
+
|
|
171
|
+
/* ruler: numbers are monospace */
|
|
172
|
+
.lines .rule { stroke: var(--hair); stroke-width: 1; }
|
|
173
|
+
.lines .tick { stroke: var(--faint); stroke-width: 1; }
|
|
174
|
+
.lines .tick.major { stroke: var(--dim); }
|
|
175
|
+
.lines .tlabel { font: 400 10px/1 var(--mono); fill: var(--dim); }
|
|
176
|
+
|
|
177
|
+
/* lanes: each in its colour, full when active, faint otherwise */
|
|
178
|
+
.lines .lane { fill: none; stroke-width: 2; stroke-linecap: round; opacity: .3; transition: opacity .15s var(--ease); }
|
|
179
|
+
.lines .lane.active { opacity: 1; }
|
|
180
|
+
.lines .lane.active.ahead { opacity: .45; }
|
|
181
|
+
.lines .lane.hit { stroke: transparent !important; stroke-width: 16; opacity: 1; cursor: pointer; }
|
|
182
|
+
.lines .lane.hit[data-active] { cursor: default; }
|
|
183
|
+
.lines .lane-end { fill: var(--page); stroke-width: 1.5; }
|
|
184
|
+
|
|
185
|
+
/* animation blocks: faint, thicker, on the active lane */
|
|
186
|
+
.lines .band { opacity: .22; transition: opacity .15s var(--ease); }
|
|
187
|
+
.lines .band.hot { opacity: .42; }
|
|
188
|
+
.lines .band-hit { fill: transparent; }
|
|
189
|
+
|
|
190
|
+
/* markers: blue dots for what you did, grey ticks for the rest */
|
|
191
|
+
.lines .mk-dot { fill: var(--blue); stroke: var(--page); stroke-width: 1; }
|
|
192
|
+
.lines .mk-tick { stroke: var(--dim); stroke-width: 1; }
|
|
193
|
+
.lines .mk-tick.hot { stroke: var(--white); }
|
|
194
|
+
.lines .mk-hit { fill: transparent; }
|
|
195
|
+
.lines .bookmark { fill: var(--fg); }
|
|
196
|
+
|
|
197
|
+
/* playhead and guides */
|
|
198
|
+
.lines .head { stroke: var(--white); stroke-width: 1; pointer-events: none; }
|
|
199
|
+
.lines .head-cap { fill: var(--white); pointer-events: none; }
|
|
200
|
+
.lines .guide { stroke-width: 1; pointer-events: none; }
|
|
201
|
+
.lines .guide-chip { pointer-events: none; }
|
|
202
|
+
.lines .guide-label { font: 500 9px/1 var(--mono); fill: var(--ink); pointer-events: none; }
|
|
203
|
+
.lines .snap { stroke: var(--white); stroke-width: 1; stroke-dasharray: 1 2; pointer-events: none; }
|
|
204
|
+
.lines .snap-dot { fill: var(--page); stroke: var(--white); stroke-width: 1; pointer-events: none; }
|
|
205
|
+
|
|
206
|
+
/* notes on the timeline */
|
|
207
|
+
.lines .note-mark { cursor: pointer; }
|
|
208
|
+
.lines .note-mark text { font: 600 8px/1 var(--mono); fill: var(--ink); pointer-events: none; }
|
|
209
|
+
|
|
210
|
+
/* The one hover tooltip: an animation block's, small, above the dock. */
|
|
211
|
+
.tip { position: fixed; z-index: 25; pointer-events: none; max-width: 240px; padding: 6px 8px; font: 400 11px/15px var(--sans); color: var(--fg);
|
|
212
|
+
background: var(--glass-strong); border: 1px solid var(--hair-2); border-radius: 8px; box-shadow: var(--shadow); animation: card-in .12s var(--ease); }
|
|
213
|
+
.tip[hidden] { display: none; }
|
|
214
|
+
.tip .row { display: flex; gap: 4px; white-space: nowrap; overflow: hidden; }
|
|
215
|
+
.tip .row b { font-weight: 500; flex: none; }
|
|
216
|
+
.tip .row span { color: var(--muted); overflow: hidden; text-overflow: ellipsis; }
|
|
217
|
+
.tip .more { color: var(--dim); }
|
|
218
|
+
|
|
219
|
+
/* ---- on the prototype ---- */
|
|
220
|
+
#wb-hl { position: fixed; pointer-events: none; border: 1.5px solid var(--amber); border-radius: 4px; z-index: 20; display: none; }
|
|
221
|
+
#wb-hl::after { content: attr(data-label); position: absolute; left: -1.5px; top: -24px; font: 500 11px/1 var(--sans);
|
|
222
|
+
color: var(--ink); background: var(--amber); padding: 4px 8px; border-radius: 999px; white-space: nowrap; }
|
|
223
|
+
#wb-hl.select { border-color: var(--white); }
|
|
224
|
+
#wb-hl.pseudo { border-style: dashed; }
|
|
225
|
+
/* The layer picker's chip, beside the pointer. */
|
|
226
|
+
#wb-layers { position: fixed; z-index: 21; pointer-events: none; min-width: 140px; max-width: 280px; padding: 4px; border-radius: 8px;
|
|
227
|
+
background: var(--glass-strong); border: 1px solid var(--hair-2); box-shadow: var(--shadow); font: 400 11px/16px var(--mono); color: var(--muted); }
|
|
228
|
+
#wb-layers[hidden] { display: none; }
|
|
229
|
+
#wb-layers .layer { padding: 2px 6px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
230
|
+
#wb-layers .layer.pseudo { padding-left: 14px; }
|
|
231
|
+
#wb-layers .layer.on { color: var(--ink); background: var(--amber); }
|
|
232
|
+
#wb-layers .more { padding: 2px 6px; color: var(--dim); }
|
|
233
|
+
#wb-hl.select::after { background: var(--white); }
|
|
234
|
+
#wb-scope { position: fixed; pointer-events: none; border: 1px dashed var(--muted); border-radius: 8px; z-index: 14; display: none; }
|
|
235
|
+
#wb-pins { position: fixed; inset: 0; pointer-events: none; z-index: 15; }
|
|
236
|
+
.canvas-pin { position: fixed; pointer-events: auto; width: 20px; height: 20px; margin: -10px 0 0 -10px; border-radius: 999px; background: var(--amber);
|
|
237
|
+
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
|
+
@media (hover: hover) { .canvas-pin:hover { transform: scale(1.12); } }
|
|
239
|
+
.canvas-pin.away { opacity: .45; }
|
|
240
|
+
@media (hover: hover) { .canvas-pin.away:hover { opacity: 1; } }
|
|
241
|
+
|
|
242
|
+
/* ---- popovers: a note, a context menu ---- */
|
|
243
|
+
.card { position: fixed; z-index: 30; width: 320px; padding: 16px; border-radius: 12px; color: var(--fg);
|
|
244
|
+
background: var(--glass-strong); -webkit-backdrop-filter: blur(24px) saturate(140%); backdrop-filter: blur(24px) saturate(140%);
|
|
245
|
+
border: 1px solid var(--hair-2); box-shadow: var(--shadow); animation: card-in .16s var(--ease); }
|
|
246
|
+
@keyframes card-in { from { opacity: 0; transform: translateY(4px); } }
|
|
247
|
+
.card[hidden] { display: none; }
|
|
248
|
+
/* The arrow points at the exact spot the note is about. */
|
|
249
|
+
.card .arrow { position: absolute; width: 12px; height: 12px; background: var(--glass-strong); border: 1px solid var(--hair-2); transform: rotate(45deg); }
|
|
250
|
+
.card[data-side="below"] .arrow { top: -7px; border-right: 0; border-bottom: 0; }
|
|
251
|
+
.card[data-side="above"] .arrow { bottom: -7px; border-left: 0; border-top: 0; }
|
|
252
|
+
.card[data-side="right"] .arrow { left: -7px; border-right: 0; border-top: 0; }
|
|
253
|
+
.card[data-side="left"] .arrow { right: -7px; border-left: 0; border-bottom: 0; }
|
|
254
|
+
|
|
255
|
+
.note-meta { display: flex; align-items: center; gap: 8px; font: 500 12px/16px var(--sans); color: var(--muted); min-width: 0; }
|
|
256
|
+
.note-meta .dot { flex: none; width: 8px; height: 8px; border-radius: 999px; }
|
|
257
|
+
.note-meta .time { flex: none; font-family: var(--mono); color: var(--fg); }
|
|
258
|
+
.note-meta .el { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--amber); }
|
|
259
|
+
.card textarea { display: block; width: 100%; margin-top: 8px; resize: none; background: transparent; border: 0; color: var(--fg);
|
|
260
|
+
font: 400 14px/20px var(--sans); outline: none; padding: 0; }
|
|
261
|
+
.card textarea::placeholder { color: var(--dim); }
|
|
262
|
+
.note-text { margin: 8px 0 0; font-size: 14px; line-height: 20px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
263
|
+
.note-clip { margin-top: 8px; font: 400 12px/16px var(--sans); color: var(--muted); }
|
|
264
|
+
.details { margin-top: 8px; font-size: 12px; line-height: 16px; color: var(--muted); }
|
|
265
|
+
.details summary { cursor: pointer; list-style: none; color: var(--dim); user-select: none; }
|
|
266
|
+
.details summary::-webkit-details-marker { display: none; }
|
|
267
|
+
.details summary::before { content: "›"; display: inline-block; width: 12px; transition: transform .15s var(--ease); }
|
|
268
|
+
.details[open] summary::before { transform: rotate(90deg); }
|
|
269
|
+
.details dl { margin: 8px 0 0; display: grid; grid-template-columns: auto 1fr; gap: 4px 8px; }
|
|
270
|
+
.details dt { color: var(--dim); }
|
|
271
|
+
.details dd { margin: 0; font-family: var(--mono); font-size: 11px; color: var(--fg); overflow-wrap: anywhere; }
|
|
272
|
+
.replies { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--hair); padding-top: 8px; }
|
|
273
|
+
.reply { font-size: 13px; line-height: 18px; color: var(--fg); }
|
|
274
|
+
.reply .who { display: block; font: 500 11px/16px var(--sans); color: var(--dim); }
|
|
275
|
+
.reply.agent .who { color: var(--blue); }
|
|
276
|
+
.note-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-top: 16px; }
|
|
277
|
+
.note-actions .status { margin-right: auto; font: 500 11px/1 var(--sans); color: var(--amber); }
|
|
278
|
+
.note-actions .status.s-acknowledged { color: var(--blue); }
|
|
279
|
+
.note-actions .status.s-resolved { color: var(--green); }
|
|
280
|
+
.note-actions .status.s-dismissed { color: var(--dim); }
|
|
281
|
+
.note-actions button { height: 32px; padding: 0 12px; white-space: nowrap; border-radius: 8px; font: 500 13px/1 var(--sans); color: var(--muted); }
|
|
282
|
+
@media (hover: hover) { .note-actions button:hover { color: var(--white); background: var(--fill-2); } }
|
|
283
|
+
.note-actions button.quiet { padding: 0 8px; color: var(--dim); }
|
|
284
|
+
@media (hover: hover) { .note-actions button.quiet:hover { color: var(--red); background: rgba(255, 107, 107, 0.1); } }
|
|
285
|
+
.note-actions button.primary { background: var(--white); color: var(--ink); }
|
|
286
|
+
@media (hover: hover) { .note-actions button.primary:hover { background: #e6e6ea; color: var(--ink); } }
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
.ctx { width: auto; min-width: 180px; padding: 4px; }
|
|
290
|
+
.ctx button { display: flex; width: 100%; justify-content: flex-start; padding: 8px; border-radius: 8px; font: 500 13px/1 var(--sans); }
|
|
291
|
+
@media (hover: hover) { .ctx button:hover { background: var(--fill); color: var(--white); } }
|
|
292
|
+
.ctx button.danger { color: var(--red); }
|
|
293
|
+
.ctx .menu-note { padding: 8px; color: var(--dim); font-size: 12px; line-height: 16px; }
|
|
294
|
+
|
|
295
|
+
@media (prefers-reduced-motion: reduce) { * { animation-duration: 0s !important; transition-duration: 0s !important; } }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Prototype</title>
|
|
7
|
+
<style>/*CSS*/</style>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="wb-stage"><div id="wb-shield" hidden></div></div>
|
|
11
|
+
<div id="wb-dock" role="region" aria-label="Retake timeline">
|
|
12
|
+
<div class="divider" role="separator" aria-orientation="horizontal" aria-label="Resize the timeline"><span></span></div>
|
|
13
|
+
<div class="head">
|
|
14
|
+
<div class="left">
|
|
15
|
+
<div class="transport">
|
|
16
|
+
<button class="play" data-a="play" aria-label="Play">
|
|
17
|
+
<svg class="i-play" viewBox="0 0 16 16" width="12" height="12"><path d="M5 3.4v9.2c0 .5.55.8.97.53l7.1-4.6a.63.63 0 0 0 0-1.06l-7.1-4.6A.63.63 0 0 0 5 3.4z" fill="currentColor"/></svg>
|
|
18
|
+
<svg class="i-pause" viewBox="0 0 16 16" width="12" height="12"><rect x="3.5" y="3" width="3" height="10" rx="1" fill="currentColor"/><rect x="9.5" y="3" width="3" height="10" rx="1" fill="currentColor"/></svg>
|
|
19
|
+
</button>
|
|
20
|
+
<span class="readout" aria-live="off"><b class="t">00:00.00</b><span class="phase">Live</span></span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="hint" aria-live="polite"></div>
|
|
24
|
+
<div id="wb-toast" role="status" hidden><span>Session cleared</span><button data-a="undo">Undo</button></div>
|
|
25
|
+
<div class="right">
|
|
26
|
+
<div class="tools" role="tablist" aria-label="Tool">
|
|
27
|
+
<button class="tool" data-tool="hand" role="tab" aria-label="Hand">
|
|
28
|
+
<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="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg>
|
|
29
|
+
</button>
|
|
30
|
+
<button class="tool" data-tool="comment" role="tab" aria-label="Comment">
|
|
31
|
+
<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="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<span class="notes-count" role="status" aria-label="No notes on this timeline" title="Notes on this timeline">
|
|
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
|
+
<span class="n">0</span>
|
|
37
|
+
</span>
|
|
38
|
+
<button class="btn fresh" data-a="fresh">Start fresh</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="body">
|
|
42
|
+
<div class="gutter" aria-label="Timelines"></div>
|
|
43
|
+
<div class="track">
|
|
44
|
+
<canvas class="lines" aria-label="Timeline"></canvas>
|
|
45
|
+
<button class="live-btn" data-a="live" hidden>Live →</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="wb-tip" class="tip" hidden></div>
|
|
50
|
+
<div id="wb-layers" hidden></div>
|
|
51
|
+
<div id="wb-hl"></div>
|
|
52
|
+
<div id="wb-scope"></div>
|
|
53
|
+
<div id="wb-pins"></div>
|
|
54
|
+
<div id="wb-note" class="card" hidden></div>
|
|
55
|
+
<div id="wb-menu" class="card ctx" hidden></div>
|
|
56
|
+
<script>/*CONFIG*/</script>
|
|
57
|
+
<script>/*JS*/</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Types for the runtime API, `window.__retake`, in the dock's app frame
|
|
2
|
+
// (CONTRACT.md, "Runtime API"). Add them to a project with
|
|
3
|
+
// `/// <reference types="retake-dev/client" />` or `"types": ["retake-dev/client"]`.
|
|
4
|
+
//
|
|
5
|
+
// In a page Retake isn't running in (any iframe the dock didn't make), it's
|
|
6
|
+
// `{ inert: true }`; with no Retake at all it's undefined.
|
|
7
|
+
|
|
8
|
+
export interface RetakeMarker {
|
|
9
|
+
t: number
|
|
10
|
+
/** "type" is one burst of typing (gaps under 800ms). */
|
|
11
|
+
end?: number
|
|
12
|
+
kind: "click" | "submit" | "route" | "focus" | "type"
|
|
13
|
+
label: string
|
|
14
|
+
selector?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RetakeClip {
|
|
18
|
+
id: string | number
|
|
19
|
+
start: number
|
|
20
|
+
end: number | null
|
|
21
|
+
kind: "transition" | "css-animation" | "waapi"
|
|
22
|
+
label: string
|
|
23
|
+
selector: string
|
|
24
|
+
component?: string
|
|
25
|
+
property?: string
|
|
26
|
+
path: Array<string | number>
|
|
27
|
+
iterations?: "infinite" | number
|
|
28
|
+
pseudoElement?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 10 Hz samples: `v` 0..1 is the share of the viewport that changed (baselined); `t` is the window start. */
|
|
32
|
+
export type RetakeActivity = Array<{ t: number; v: number }> & { step?: number; t0?: number; values?: number[] }
|
|
33
|
+
|
|
34
|
+
export interface RetakeTimeline {
|
|
35
|
+
now: number
|
|
36
|
+
end: number
|
|
37
|
+
viewport: { w: number; h: number }
|
|
38
|
+
/** User actions only. */
|
|
39
|
+
markers: RetakeMarker[]
|
|
40
|
+
clips: RetakeClip[]
|
|
41
|
+
activity: RetakeActivity
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RetakeRuntime {
|
|
45
|
+
readonly inert?: false
|
|
46
|
+
version: string
|
|
47
|
+
/** The virtual clock, in ms. */
|
|
48
|
+
now(): number
|
|
49
|
+
timeline(): RetakeTimeline
|
|
50
|
+
/** Clips on that element and its descendants, looping ones included. */
|
|
51
|
+
clipsFor(target: Element | string): RetakeClip[]
|
|
52
|
+
/** The latest clip running at `t` (on `selector`, or inside it); `offset` is ms into it. */
|
|
53
|
+
clipAt(t: number, selector?: string): { clip: RetakeClip; offset: number } | null
|
|
54
|
+
/** True only while playing at the live edge. */
|
|
55
|
+
isInteractive(): boolean
|
|
56
|
+
isPaused(): boolean
|
|
57
|
+
/** Called on every play/pause change. Returns an unsubscribe. */
|
|
58
|
+
onPlayState(fn: (state: { playing: boolean; now: number }) => void): () => void
|
|
59
|
+
/** Play from here: replays the recorded future, then carries on live. */
|
|
60
|
+
play(): void
|
|
61
|
+
pause(): void
|
|
62
|
+
/** Starts the timeline, or resumes recording from its end. */
|
|
63
|
+
record(): void
|
|
64
|
+
/** Go to moment `t` (ends a preview). False before anything was recorded. */
|
|
65
|
+
seek(t: number, play?: boolean): boolean | void
|
|
66
|
+
setToolActive(on: boolean): void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
declare global {
|
|
70
|
+
interface Window {
|
|
71
|
+
__retake?: RetakeRuntime | { readonly inert: true }
|
|
72
|
+
}
|
|
73
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Types for `retake-dev`: the Vite plugin and the pieces a server shares to
|
|
2
|
+
// dock Retake into pages it serves (src/plugin.js, src/core.js).
|
|
3
|
+
import type { Transform } from "node:stream"
|
|
4
|
+
import type { Plugin } from "vite"
|
|
5
|
+
|
|
6
|
+
/** Options for the Vite plugin. Dev only: it does nothing in builds. */
|
|
7
|
+
export interface RetakeOptions {
|
|
8
|
+
/** `false` turns the plugin off (the pages are served as they are). Default `true`. */
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
/**
|
|
11
|
+
* Each timeline keeps its own version of the code: stepping into a
|
|
12
|
+
* timeline checks its code out on disk. It rewrites your source files.
|
|
13
|
+
* Default `false`.
|
|
14
|
+
*/
|
|
15
|
+
codeBranches?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* The token mutating `/__retake/` requests must carry (header
|
|
18
|
+
* `x-retake-token`). Default: a random one per server start.
|
|
19
|
+
*/
|
|
20
|
+
token?: string
|
|
21
|
+
/** Where `.retake/` (sessions, recordings, notes) goes. Default: Vite's root. */
|
|
22
|
+
root?: string
|
|
23
|
+
/** `false` hides the "Retake timeline docked at …" line on startup. Default `true`. */
|
|
24
|
+
banner?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The Vite plugin: the dock for page loads, the time runtime in the dock's
|
|
29
|
+
* frame, and the session API under `/__retake/`.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { defineConfig } from "vite"
|
|
33
|
+
* import { retake } from "retake-dev"
|
|
34
|
+
* export default defineConfig({ plugins: [retake()] })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function retake(options?: RetakeOptions): Plugin[]
|
|
38
|
+
export default retake
|
|
39
|
+
|
|
40
|
+
/** How the runtime is set up for the server it runs behind (`RT` in the runtime). */
|
|
41
|
+
export interface RuntimeConfig {
|
|
42
|
+
/**
|
|
43
|
+
* How the dock's frame is known: `"url"` (its URL carries `__wb=app`, the
|
|
44
|
+
* Vite plugin's way) or `"header"` (the server tells by `Sec-Fetch-Dest`).
|
|
45
|
+
* Default `"url"`.
|
|
46
|
+
*/
|
|
47
|
+
marker?: "url" | "header"
|
|
48
|
+
/** When the clock starts: at DOMContentLoaded (`"dcl"`, default) or at `load`. */
|
|
49
|
+
bootAt?: "dcl" | "load"
|
|
50
|
+
/** The dev server's own traffic (regular expression sources), never recorded or held. */
|
|
51
|
+
exemptUrls?: string[]
|
|
52
|
+
/** Scripts added after load are held to their recorded moment. Default `false`. */
|
|
53
|
+
holdScripts?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* The Next.js major version, for its dev debug channel; `"auto"`: the
|
|
56
|
+
* runtime asks Next's client. Default `null` (not Next).
|
|
57
|
+
*/
|
|
58
|
+
next?: number | "auto" | null
|
|
59
|
+
/** This page's kept copy on the front server. Default `null`. */
|
|
60
|
+
docId?: string | null
|
|
61
|
+
/** Whether this page is that kept copy. Default `false`. */
|
|
62
|
+
docStored?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The time runtime as JavaScript source: one IIFE, to run as the page's first script. */
|
|
66
|
+
export function runtimeSource(rt?: RuntimeConfig): string
|
|
67
|
+
|
|
68
|
+
/** Options for the dock page. */
|
|
69
|
+
export interface ShellOptions {
|
|
70
|
+
/** Show the code-branch controls (needs a server running with code branches). */
|
|
71
|
+
codeBranches?: boolean
|
|
72
|
+
/**
|
|
73
|
+
* The path the dock's frame always loads (a static build that ships the
|
|
74
|
+
* app as a page of its own). Default: the page's own URL.
|
|
75
|
+
*/
|
|
76
|
+
appPage?: string
|
|
77
|
+
/** How the dock marks its frame; see {@link RuntimeConfig.marker}. Default `"url"`. */
|
|
78
|
+
marker?: "url" | "header"
|
|
79
|
+
/** Rebuilds ask the server for the page as it was recorded (front server only). */
|
|
80
|
+
docs?: boolean
|
|
81
|
+
/** The token mutating `/__retake/` requests carry. */
|
|
82
|
+
token?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The dock page: a complete HTML document with the app in a frame. */
|
|
86
|
+
export function shellHtml(options?: ShellOptions): string
|
|
87
|
+
|
|
88
|
+
export interface RuntimeTagOptions {
|
|
89
|
+
/** The runtime's setup. */
|
|
90
|
+
rt?: RuntimeConfig
|
|
91
|
+
/** A CSP nonce for the `<script>` tag. */
|
|
92
|
+
nonce?: string | null
|
|
93
|
+
/** The script to wrap. Default: the guarded runtime for `rt`. */
|
|
94
|
+
script?: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The `<script data-retake>` tag a server injects into a page: it removes
|
|
99
|
+
* itself, then runs the runtime only in a frame the dock made.
|
|
100
|
+
*/
|
|
101
|
+
export function runtimeTag(options?: RuntimeTagOptions): string
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A byte stream transform that inserts `tag` into an HTML document as it
|
|
105
|
+
* streams: after the `<head>` opening tag (and a `<meta charset>` right after
|
|
106
|
+
* it), else before the first `<body>`, `<script>`, `<link>`, `<meta>`,
|
|
107
|
+
* `<style>` or `<title>`. A compressed body (`encoding`: gzip, br, deflate,
|
|
108
|
+
* zstd) is decoded first and the output is plain. Throws for an encoding it
|
|
109
|
+
* can't decode.
|
|
110
|
+
*/
|
|
111
|
+
export function injectHtml(tag: string, options?: { encoding?: string }): Transform
|