For this project, I performed problem analysis, made wireframes, interactive designs, and the MVP implementation. The CTO helped me cut scope for the MVP and implemented some follow-up changes.
Dark lays its constructs (code and data schemas) in floating boxes. Users spend their time panning, dragging, jumping around their project space instead of coding. Why do they do that?
Often users need to drag databases close to their code because DB::set requires the data inserted to match the schema exactly.
When a function has code referring to a DB, the experience is even worse!
Users can create functions, which will be accessible throughout their project. But these functions are NOT accessible within the main space.
Workers are like async functions that appear in the main space. But that doesn't make using them any easier.
Problems we face today are created by people in the past with the best intentions
The floating boxes (of code and DBs) are one day suppose to have dots/lines between them to show live traffic. They will organize code for users, replacing folders and files. File systems, he theorized, are arbitrary ways of splitting code. They give users more problems to worry about (such as dependency cycles) than solutions.
If the endpoints and datastores are landmarks, then the path data takes are streets and highways.
The path of a single trace is a trip.
Over time we can construct a cumulative flow illustrating the popularity of certain paths. The user will know which parts of the codebase to devote more resources.
At the time of this project (early 2019). Now we have many users with live traffic
How can we help users understand their codebase? So they may write, debug, and edit code with ease?
POST /track gets hit it when you've completed a habit. Most of the execution is offloaded to a queue (the "didHabit" worker). The worker will add an entry to the Tracker DB, and send you a meme if you've met your weekly goal.
![]() | ![]() |
We start off with POST /track, its code it references Habits DB with DB::get. And emits an event to "didHabit" worker.
Are we there yet?
Have we mapped out all the paths we want to capture? When POST /track gets hit, most of its execution is offloaded to a queue (the "didHabit" worker). The worker will add an entry to the Tracker DB, and send you a meme if you've met your weekly goal.
![]() |
![]() |
What is it that is missing? Send meme or encouragement!
... and how do we get there?
Looks like they are functions. And a problem our users face is:
Functions are NOT accessible within the main space; they exist in their own little world. Nor are they accessible from the code that uses them. They can only be reached through the sidebar.
![]() |
![]() |
Now, when the user clicks into a function's reference, they can see the code.
So far references are just boxy links. How can we use the space to provide useful information to the user?
Sign designers anticipate a driver's needs as they drive by. Do they need a bathroom break? More gas? A snack? What are our user's needs?
Reach out @aliceaskwhy, if you want to share badly designed highway signs.
Users drag DBs closer to their code to check schema.
Users want to see what happens to their code when it passes through a function.
Users want to know what a worker expects for its input (event : Any)
Let's bring some color consistency and typographic hierarchy.
![]() |
![]() |
Now how should we arrange our references in relation to the code?
Going down the path horizontally might not work. Live values are on the left!
For now let's draw all the references on the right side with incoming and outgoing arrows. We'll think more about their ideal placement later.
Overall the feedback was positive. New users pointed out it was one of the features they felt made Dark unique. Existing users even discovered an unintended use case. They used it to depreciate functions and migrate DB schemas.
But there were several things they didn't like:
A simple fix can be to color the arrow and box differently for incoming and outgoing references.
Now that references have proven their worth, we might be able to move live values.
The cursor is at a different place when users look at references than when they look at live values.
A user looks at incoming references to see its usage. The code (and consequently live values) can be a black box. We don't need to show live values in that case.
When a user edits code, they want to know what is happening to the data. We fade out the incoming references and show the live value then.
After the scope was cut, and we implemented references for workers and functions. But there was no plan to implement other cut features nor execute on revisions.
Without moving our incoming references on the opposite side of outgoing referencesm, the map/navigation analogy is broken.