I hate planning desk setups.
Every time I buy a new monitor or dock, I spend 20 minutes Googling "does USB-C carry DisplayPort signal" or "can I daisy-chain these two monitors." So I built a tool to do it for me.
DeskFlow — a browser-based diagram tool where you drag equipment onto a canvas, click ports to connect them, and instantly see whether the cable is physically possible + what to buy.
What it does
- Drag equipment from a library onto a canvas
- Click two ports to connect them — compatibility is checked automatically
- Incompatible connections are rejected (e.g. HDMI out → DP in via passive adapter is blocked, two bidirectional power ports can't connect to each other)
- Shopping list panel auto-generates with cable names and prices from a DB
Stack
- Flutter Web — single codebase deployed as a web app without learning a JS framework
- Supabase — equipment DB (33 devices, 11 cable types). anon key only, all reads. No auth needed.
- Vercel — static hosting with a
vercel.jsonrewrite rule for SPA routing
The interesting problems
1. Gesture arena conflicts in Flutter Web
The canvas uses a GestureDetector with HitTestBehavior.opaque for panning. Port chips sit on top of the canvas as overlaid widgets.
Problem: tapping a port chip would get stolen by the canvas gesture detector before the port's tap handler fired.
Fix: replaced GestureDetector with raw Listener (onPointerDown/Move/Up). Listeners don't participate in the gesture arena — they always fire.
// Before: lost to gesture arena
GestureDetector(
onPanUpdate: (d) => _handleResize(d.delta),
child: divider,
)
// After: always fires
Listener(
onPointerMove: (e) => _handleResize(e.delta),
child: divider,
)
2. Port compatibility model
Ports have a type (hdmi, dp, usbC, thunderbolt4, usbA, power, ethernet, sdcard) and a direction (input, output, bidirectional).
- TB4 and USB-C share the same physical connector → treated as compatible
- USB-C ↔ HDMI/DP/USB-A → allowed (adapters exist)
- DP(output) → HDMI(input) → allowed (passive adapter)
- HDMI(output) → DP(input) → blocked (needs active converter)
- Two bidirectional power ports → blocked (two chargers can't charge each other)
- SD card ↔ SD card only → no cross-type adapters
TB4 ports fall back to usbC when looking up the cable catalog:
static String _normalizeCableType(String type) =>
type == 'thunderbolt4' ? 'usbC' : type;
3. SPA routing on Vercel
Flutter Web produces a single index.html. Direct URL access returns 404 without this in vercel.json:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
What's next
- Setup templates (home office, streaming, video editing)
- Mobile/touch support
- Shopping cart total / budget summary panel
Happy to answer questions about any of the Flutter Web specifics — it's a surprisingly capable target that doesn't get enough coverage.

Top comments (0)