[proof of concept] Floating Widgets (Example: Treeview, “OrbitCal” Calendar)

I’ve been experimenting with extending SilverBullet’s UI and wanted to share a small proof-of-concept I put together - a floating, draggable calendar widget.

I call it OrbitCal, and while it doesn’t do much (yet), it’s a neat little demonstration of what’s possible with some creative use of HTML , CSS, and a dash of JavaScript inside the SilverBullet environment.

What it does

Displays a tiny dark-themed calendar overlay
Can be dragged anywhere on screen
Is resizable, dockable, and toggleable (so it won’t clutter your workspace)

Keeps track of the current date and allows month navigation

:light_bulb: Why I built it

This isn’t meant to be a full calendar system (duh) - it’s simply a testbed for draggable, floating widgets. The goal is to explore how modular, overlay-style tools could live inside SilverBullet: think clocks, sticky notes, quick task panels, or even mini dashboards.

Technical overview

Currently built entirely in JS (I know, I know, Lua is the future of Silverbullet - but one step at a time)
Designed to run inside SilverBullet’s JS-sandbox — no external dependencies

drop this orbitcal.js into your space folder

export function Calendar() {
  if (document.querySelector("#myWindow")) return;

  const w = document.createElement("div");
  w.id = "myWindow";
  w.innerHTML = `
    <div id="myWindowHeader">
      <button id="prevBtn" class="navBtn">◀</button>
      <span id="monthYear"></span>
      <button id="nextBtn" class="navBtn">▶</button>
      <button id="closeBtn">✕</button>
    </div>
    <div class="content">
      <div class="calendar-grid" id="calendarGrid"></div>
    </div>
    <div id="resizeGrip"><div id="resizeHandle"></div></div>
  `;
  document.body.appendChild(w);

  // Load stored position
  const pos = JSON.parse(localStorage.getItem("calendarPos") || "{}");
  w.style.left = pos.left !== undefined ? pos.left + "px" : "30px";
  w.style.top = pos.top !== undefined ? pos.top + "px" : "100px";

  // Styles
  const s = document.createElement("style");
  s.textContent = `
    #myWindow {
      position: fixed;
      width: 250px;
      height: 260px;
      background: rgba(30,30,30,.9);
      color: #fff;
      border-radius: 12px;
      box-shadow: 0 0 4px rgba(255,255,255,.6),0 0 25px rgba(0,0,0,.8);
      z-index: 9999;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      transition: .2s;
      font-family: system-ui,sans-serif;
    }
    #myWindowHeader {
      padding: 8px 0;
      background: rgba(255,255,255,.1);
      cursor: move;
      user-select: none;
      text-align: center;
      font-weight: bold;
      border-bottom: 1px solid rgba(255,255,255,.1);
      flex-shrink: 0;
      font-size: 1.1em;
      position: relative;
    }
    .navBtn,#closeBtn {
      background: transparent;
      border: none;
      color: #fff;
      font-size: 0.8em;
      cursor: pointer;
      font-weight: bold;
    }
    .navBtn:hover,#closeBtn:hover { color: #ff5555; }
    #prevBtn { position: absolute; left: 20px; top: 8px; }
    #nextBtn { position: absolute; right: 20px; top: 8px; }
    #closeBtn { position: absolute; right: -13px; top: 4px; line-height: 1; }
    .content {
      padding: 5px;
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .calendar-grid {
      display: grid;
      grid-template-columns: repeat(7,1fr);
      gap: 6px;
      text-align: center;
      font-size: .9em;
      flex: 1;
    }
    .calendar-grid div {
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 6px;
      background: rgba(255,255,255,.05);
    }
    .calendar-grid .day-name {
      font-weight: bold;
      background: none; 
    }
    .calendar-grid .today {
  background: #ff5555;
  color: #fff;
  box-shadow: 0 0 10px #ff5555;
  font-weight: bold;
  text-shadow: 0 1px 1px #000;
}

    #resizeGrip {
      position: absolute;
      right: 0;
      bottom: 0;
      width: 15px;
      height: 15px;
      cursor: se-resize;
    }
    #resizeHandle {
      width: 8px;
      height: 8px;
      background: rgba(255,255,255,.7);
      border-radius: 50%;
      box-shadow: 0 0 2px rgba(0,0,0,.5);
      position: absolute;
      right: 6px;
      bottom: 6px;
    }
    #resizeHandle:hover { background: rgba(255,0,0,1); }
  `;
  document.head.appendChild(s);

  // Calendar data
  let y = (new Date()).getFullYear(), m = (new Date()).getMonth();
 const render = () => {
  const M = ["January","February","March","April","May","June","July","August","September","October","November","December"];
  const D = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
  const today = new Date();
  const t = today.getDate(), todayM = today.getMonth(), todayY = today.getFullYear();

  document.getElementById("monthYear").textContent = `${M[m]} ${y}`;
  const g = document.getElementById("calendarGrid");
  g.innerHTML = "";

  // Day names
  D.forEach(d => g.appendChild(Object.assign(document.createElement("div"), { textContent: d, className: "day-name" })));

  // First day of the month (0=Sun..6=Sat) → shift so Monday=0
  let firstDay = new Date(y, m, 1).getDay();
  firstDay = firstDay === 0 ? 6 : firstDay-1;

  const daysInMonth = new Date(y, m+1, 0).getDate();

  // Empty slots before first day
  for(let i=0;i<firstDay;i++) g.appendChild(document.createElement("div"));

  // Days
  for(let i=1;i<=daysInMonth;i++){
    const e = document.createElement("div");
    e.textContent = i;

    // Determine weekday (0=Mon..6=Sun)
    const weekdayIndex = (firstDay + i - 1) % 7;
    if(weekdayIndex === 6) e.style.background = "rgba(255,0,0,0.2)"; // Sunday red
    if(i===t && m===todayM && y===todayY) e.classList.add("today");

    g.appendChild(e);
  }
  const monthYearSpan = document.getElementById("monthYear");
monthYearSpan.addEventListener("click", () => {
  y = (new Date()).getFullYear();
  m = (new Date()).getMonth();
  render();
});

};

  render();
  document.getElementById("prevBtn").onclick = ()=>{ m--; if(m<0){m=11;y--;} render(); };
  document.getElementById("nextBtn").onclick = ()=>{ m++; if(m>11){m=0;y++;} render(); };

  // Drag
  const M_bounds = {l:10,t:60,r:30,b:10}, snap=100;
  const drag = ()=>{
    const h = w.querySelector("#myWindowHeader");
    let ox=0, oy=0, dr=false;
    const lim=()=>{const r=w.getBoundingClientRect(),W=innerWidth,H=innerHeight;
      w.style.left=Math.max(M_bounds.l,Math.min(r.left,W-r.width-M_bounds.r))+"px";
      w.style.top=Math.max(M_bounds.t,Math.min(r.top,H-r.height-M_bounds.b))+"px"};
    const st=p=>{dr=true;const r=w.getBoundingClientRect(),c=p.touches?p.touches[0]:p;ox=c.clientX-r.left;oy=c.clientY-r.top;w.style.transition="none"};
    const mv=p=>{if(!dr)return;p.preventDefault();const c=p.touches?p.touches[0]:p;w.style.left=c.clientX-ox+"px";w.style.top=c.clientY-oy+"px"};
    const sp=()=>{if(!dr)return; dr=false; w.style.transition=".2s"; lim();
      const r = w.getBoundingClientRect();
      localStorage.setItem("calendarPos", JSON.stringify({left:r.left, top:r.top}));
    };
    h.onmousedown=st; h.ontouchstart=st;
    addEventListener("mousemove", mv); addEventListener("touchmove", mv);
    addEventListener("mouseup", sp); addEventListener("touchend", sp); addEventListener("resize", lim);
    
  };

  // Resize
  const resize = ()=>{
    const g = w.querySelector("#resizeGrip"); let rsz=false,sx,sy,sw,sh;
    g.onmousedown = e => { rsz=true; sx=e.clientX; sy=e.clientY; const r=w.getBoundingClientRect(); sw=r.width; sh=r.height; e.preventDefault(); };
    onmousemove = e => { if(!rsz) return; w.style.width=Math.max(250,sw+e.clientX-sx)+"px"; w.style.height=Math.max(260,sh+e.clientY-sy)+"px"; };
    onmouseup = ()=> rsz=false;
  };

  drag(); resize();
  document.getElementById("closeBtn").onclick = ()=> w.remove();
}

export function ToggleCalendar(){
  const w = document.querySelector("#myWindow");
  w ? w.remove() : Calendar();
}

and this space-lua in one of your pages:

command.define {
  name = "Calendar: Toggle",
  key = "Ctrl-Alt-C",
  run = function()
    local jsInit = js.import("/.fs/Library/orbitcal.js")
    jsInit.ToggleCalendar()
  end
}

Reload and run the Calendar: Toggle command.

What’s next

  • Future ideas include: Integrating with journal pages
  • Creating a lightweight “widget API” so others can build similar floating tools

This is still rough around the edges, but it’s functional and fun, a small taste of what’s possible when you blend SilverBullet’s flexibility with some UI flair.

I’d love feedback, suggestions, or even collaborations if someone wants to expand on the concept and knows more LUA, JS, HTML and CSS than me.

5 Likes

I forgot to mention, it’s also mobile friendly:

And, as a thought experiment, imagine all Plugs & Widgets in the future being Movable, Resizable, and Dockable:

You need to work with Treeview open, or other plugins which usually takes up one third of your screen - but you also don’t want to give up your precious screen real estate. Who does? So instead of turning your plugs on and off every time, imagine simply dragging things around until everything fits: Resize it, dock it, or let it float.

The idea’s simple: your workspace should adapt to you, not demand a sacrifice of tabs, patience, or sanity.

1 Like

The concept for floating widgets is very interesting indeed! Congrats on putting this together.

But, I gotta say that what really blew my mind in your post was your strategy to run JavaScript code. Simple and creative.

Currently, as far as I know, this is the only way to do it (for now). The main issue with Silverbullet .js support is that every JS script is sandboxed, locked safely inside its own little box, unable to poke its nose outside (correct me if i’m wrong). Great for security, slightly annoying for progress.

To make real integration possible, some features need to be exposed through a native API, so every plugin or widget can actually talk to a systemwide API. Right now, each JS lives in its own padded cell, doing its job quietly and safely, which is exactly how it should work for a secure environment.

Still, there’s a middle ground: a few system-wide native JS modules that everyone can call when needed. That way, plugins stay safe in their boxes, but at least they share a common language. That’s the plan for the future, controlled freedom, if you will to put it like that.

1 Like

Very nice indeed. Would be fun to see it in combination with the calendar widget that I use for my journal:

which calendar widget do you mean, I could only find the journal navigation on the provided link.

The journal widget looks essentially the same. Didn’t make it public yet.

1 Like

Made some progress last two days, so that I can share with you the following Extentsion to Move&Resize* the TreeView Plug - by @joekrill

* - it also works on Mobile

1 Like

Overall, excellent! Yet there are a few minor issues at the phenomenological level, which should be easily solvable in the code:

  1. When invoked via a shortcut key instead of a button, the window cannot be moved. It seems that the shortcut key is defined in the original plugin’s .plug.js file. How can it be updated in space-lua to perform the same action as clicking the button?
command.update {
  name = "Tree View: Toggle",
  run = function()
    editor.invokeCommand "Tree View: Toggle"
    js.import("/.fs/Library/PanelDragResize.js").enableDrag()
  end,
  key = "Ctrl-Alt-b",
  mac = "Cmd-Alt-b",
  priority = 0
}

above space-lua does not work -_-||

  1. When the window is moved, the document automatically scrolls upward. (I’m not sure if this issue is caused by a conflict with one of my own plugins.)
  1. how about this as solution for your key bindings?
command.update {
  name = "Tree View: Toggle",
  key = "",
  mac = "",
  hide = true
}

command.define {
  name = "Tree View: Toggle Move&Resize",
  key = "Ctrl-Alt-b",
  mac = "Cmd-Alt-b",
  run = function()
        editor.invokeCommand "Tree View: Toggle"
        js.import("/.fs/Library/PanelDragResize.js").enableDrag()
       end
}

If you update “Tree View: Toggle” with itself I think it will run into some sort of infinite loop sort of things.

2.I noticed that behaviour too in some browsers. The issue is that the SB-Panel where the Treeview lives was never designed to be draggable or resizable. Its HTML structure and internal selectors weren’t meant to be manipulated that way. What we’re doing here is essentially a CSS/JS hack: two <div> elements are synchronised to move and resize together. One of these, .sb-top, is technically “borrowed” from the top banner and used as a handle for our floating window.
This setup is a hybrid of CSS and JavaScript that simulates a draggable, resizable window inside SilverBullet. Since the original .sb-panel layout wasn’t built for that, the logic piggybacks on existing DOM elements to create the illusion of proper window management.
The JavaScript function enableDrag() handles the heavy lifting: it listens for mouse and touch events to move both panels in sync and resize them relative to one another. It tracks cursor position near the right and bottom edges to trigger resize mode, while normal dragging keeps their relative offset intact.
The CSS part takes care of presentation - adding shadows, rounded corners, and dynamic cursors (grab, move, resize) to make the window feel interactive. Meanwhile, the JS updates inline styles like width, height, top, and left in real time as you drag or resize.
In short, this is a functional illusion: we’re faking a proper windowing system inside a static layout without altering SilverBullet’s core renderer. It works remarkably well, considering it was never meant to - though occasionally the DOM reminds us who’s really in charge.

@ChenZhu-Xie I’ve updated the library with minor bug fixes and added the new pseudo Command with the keybindings and JS Load: Tree View: Toggle Move&Resize

1 Like

Now the shortcut keys are working perfectly!
waiting automatic scrolling during drag to be fixed :slight_smile: .

I suddenly came up with several possible uses for floating widgets:

  1. hover over = link preview = Hook Lua on AST nodes?
  2. floating top widget: TOC = Floating header/footer
  3. floating bottom widget: Page Backlinks, (for instant check)
  4. sidenote / marginnote, footnote ?.. Plug idea: Side page · Issue #245 · silverbulletmd/silverbullet · GitHub and Navigation on left side? - #3 by ChadDa3mon
1 Like

The use cases could be endless. But the idea is to create an API for floating widgets, well integrated in Silverbullet, so each user can create floating widgets and tools themselves without needing to “hack” CSS and JS.

Well at least that is what I try to pursue.

That’s why this is only a proof of concept to see how people react to it and receive the idea.

It’s definitely not for everyones use case, but to have it as an option IMHO is a good idea.

Here is another use case of a StickyNote like widget i’m also experimenting with:

3 Likes

I figured out why the “jump to top of the page” behaviour happens. It’s because like i mentioned the Header is “borrowed” from sb-top, and if in Silverbullet you click on the sb-top header, it jumps to the start of the page. i need to check if i somehow can disable this bihaviour using js.

1 Like