Multicolumn support

Hi Guys, I'd really like to create some multi column support. A sidebar or 3 columns would help me a lot. In each column I would love to be able to query and use lua like I'm used to.

Can someone help me get started with this? Tables don't cut it since I can't do queries within tables.

Can you please detail or describe, what exactly are you trying to do?
I don't quite understand.

Doesn't the Advanced Panel Control and opening pages in the SidePanels already cover that ?

Or do you want multiple columns of text, inside a single page?

I've tried Advanced Panel Control and Window Management (Mr-xRed), but I get the error "failed to initialize UnifiedFloating.js module"

Interesting. I havenโ€™t saw that error only when installing it first and didnโ€™t refreshed the page so that the .js can be loaded. After reloading the page, I didnโ€™t had any issues with the .js loading.

After installing the Advanced Panel library did you install also the a Floating Page library. In the later are the commands included to open pages/links/etc in the sidepanels.

Iโ€™d recommend you give it another try and if you encounter any errors, feel free to reach out.

Back on topic for the 3 columns: if I'm correct your plugin creates sidebars on the far left and far right of the screen right? That's not what I like to achieve. I like to have the content in columns within the width of the page itself. I made a screenshot to illustrate:

Instead of everything below eachother:

You have couple of options here bun none of them are exactly what you are looking for. and none of them will satisfy your needs.

I tried to vibe-code two of these options with mixed & unsatisfactory results.
But if you spend more time maybe you can use my examples(proof of concepts) bellow to get you started and fine-tune and iron out the issues.

Option 1

  • Pure CSS (this comes the closest to what you want, but it has some limitations and issues)
  • you can use #col1 #col2 #col3 etc. to assign a column to each .cm-line, and style that line as if it were a column inside a flex container.
  • this will mess up the cursor navigation because code-mirror inside silverbullet doesn't know columns, so it thinks those are single lines.
  • and many more caveats and issue.

here is the [space-style] to try it out.

/* โ”€โ”€ Layout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */

.cm-content:has(a[data-tag-name="col1"]) {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1rem;
}

.cm-line:has(a[data-tag-name="col1"]),
.cm-line:has(a[data-tag-name="col2"]),
.cm-line:has(a[data-tag-name="col3"]) {
  flex: 1;
  min-width: 0;
}

/* All non-col lines go full width below the columns */
.cm-content:has(a[data-tag-name="col1"]) .cm-line:not(:has(a[data-tag-name^="col"])) {
  flex: 0 0 100%;
}

/* โ”€โ”€ Hide hashtag visually, keep it measurable for CodeMirror โ”€โ”€ */

.cm-line:has(a[data-tag-name^="col"]) .sb-hashtag {
  opacity: 0;
  font-size: 0.1px;   /* near-zero but NOT zero โ€” CM can still measure it */
  line-height: 0;
  pointer-events: none;
  user-select: none;
  vertical-align: middle;
}

/* Keep the cursor/caret visitable โ€” don't let the tag eat clicks */
.cm-line:has(a[data-tag-name^="col"]) .sb-hashtag-text {
  pointer-events: none;
  user-select: none;
}

Option 2

  • you create a contained html widget in which you can display either plain text, HTML or custom HTML widgets.
  • this wont work with markdown widgets or queries directly, but you can create another html widget to display a query inside your widget.

here is the implementation and some exmaples:

-- priority: 1

-- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
-- widgets.columns(col1, col2, col3, ...)
--
-- Usage:
--   ${widgets.columns("html content", "html content", "html content")}
--
-- Optional: pass a config table as the LAST argument to customize:
--   ${widgets.columns("col1", "col2", "col3", {gap="2rem", valign="stretch", dividers=true})}
--
-- Config options:
--   gap      = CSS gap value          (default: "1.5rem")
--   valign   = align-items value      (default: "flex-start")
--   dividers = true/false             (default: false)
--   widths   = {"30%","40%","30%"}    (default: equal flex:1 for all)
-- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function widgets.columns(...)
  local args = {...}
  
  -- Check if last arg is a config table
  local opts = {}
  if type(args[#args]) == "table" and not args[#args].html then
    opts = table.remove(args)
  end

  local gap      = opts.gap      or "1.5rem"
  local valign   = opts.valign   or "flex-start"
  local dividers = opts.dividers or false
  local widths   = opts.widths   or {}

  -- Outer wrapper
  local html = "<div style='" ..
    "display:flex;" ..
    "flex-wrap:wrap;" ..
    "gap:" .. gap .. ";" ..
    "align-items:" .. valign .. ";" ..
    "'>"

  for i, content in ipairs(args) do
    -- Convert widget objects to their html, or use raw string
    local innerHtml = ""
    if type(content) == "table" and content.html then
      innerHtml = content.html
    elseif type(content) == "string" then
      innerHtml = content
    else
      innerHtml = tostring(content)
    end

    -- Per-column width: use widths table if provided, else flex:1
    local widthStyle = ""
    if widths[i] then
      widthStyle = "flex:0 0 " .. widths[i] .. ";width:" .. widths[i] .. ";"
    else
      widthStyle = "flex:1;min-width:0;"
    end

    -- Optional divider between columns (not before first)
    local dividerStyle = ""
    if dividers and i > 1 then
      dividerStyle = "border-left:1px solid var(--hr-color, #ccc);padding-left:" .. gap .. ";"
    end

    html = html ..
      "<div style='" .. widthStyle .. dividerStyle .. "'>" ..
        innerHtml ..
      "</div>"
  end

  html = html .. "</div>"

  return widget.new {
    display = "block",
    html = html
  }
end

-- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
-- widgets.queryTable(results, opts)
--
-- Renders a query result as an HTML table.
--
-- Usage:
--   ${widgets.queryTable(query[[from index.tag "page" limit 5]])}
--
-- Options (all optional):
--   cols    = {"ref","name","size"}   -- which fields to show (default: auto-detect)
--   headers = {"Page","Name","Size"}  -- custom header labels (default: field names)
-- โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function widgets.queryTable(results, opts)
  opts = opts or {}

  if type(results) ~= "table" or #results == 0 then
    return widget.new {
      display = "block",
      html = "<p style='color:var(--text-muted);font-style:italic;'>No results.</p>"
    }
  end

  -- Auto-detect columns from first result if not specified
  local cols = opts.cols
  if not cols then
    cols = {}
    for k, _ in pairs(results[1]) do
      -- Skip noisy internal fields
      if k ~= "itags" and k ~= "tags" and k ~= "tag" and k ~= "contentType" then
        table.insert(cols, k)
      end
    end
    table.sort(cols)
  end

  local headers = opts.headers or cols

  -- Build table HTML
  local html = [[
    <table style='
      width:50%;
      border-collapse:collapse;
      font-size:0.85em;
    '>
    <thead>
      <tr style='border-bottom:2px solid var(--hr-color);'>
  ]]

  for i, h in ipairs(headers) do
    html = html .. "<th style='text-align:left;padding:6px 10px;color:var(--text-muted);font-weight:600;'>" .. h .. "</th>"
  end
  html = html .. "</tr></thead><tbody>"

  for ri, row in ipairs(results) do
    local rowBg = ri % 2 == 0 and "background:var(--modal-background-color);" or ""
    html = html .. "<tr style='" .. rowBg .. "border-bottom:1px solid var(--hr-color);'>"
    for _, col in ipairs(cols) do
      local val = row[col]
      if type(val) == "table" then
        val = table.concat(val, ", ")
      else
        val = tostring(val or "")
      end
      -- Make ref fields into links
      if col == "ref" then
        val = "<a href='/" .. val .. "' style='color:var(--link-color);'>" .. val .. "</a>"
      end
      html = html .. "<td style='padding:6px 10px;'>" .. val .. "</td>"
    end
    html = html .. "</tr>"
  end

  html = html .. "</tbody></table>"

  return widget.new { display = "block", html = html }
end

Now you can use it inside widgets.columns cleanly:

${widgets.columns(
  "<p>Left plain HTML</p>",
  widgets.queryTable(query[[from index.tag "page" limit 5]]),
  "<p>Right plain HTML</p>",
  {gap="2rem", dividers=true}
)}

Or with specific columns only:

${widgets.columns(
  widgets.queryTable(
    query[[from index.tag "page" limit 5]],
    {cols={"ref","lastModified"}, headers={"Page","Last Modified"}}
  ),
  "<p>Some notes</p>",
  {widths={"70%","30%"}}
)}

Test with HTML & dividers

${widgets.columns(
"<h3>๐Ÿ“š Reading</h3><p>Some notes here...</p>",
"<h3>๐ŸŽฌ Watching</h3><p>Other content...</p>",
"<h3>๐ŸŽฎ Playing</h3><p>More content...</p>",
{gap="2rem", dividers=true}
)}

Test with plain text

${widgets.columns(
"It began on a Tuesday, arguably the most productive day for feline kind. Mittens, a striped tabby with a penchant for keyboard shortcuts, discovered the open laptop. It wasn't running a standard text editor; it was running **SilverBullet**. The cursor blinked invitingly, a digital laser pointer waiting to be chased.",
"Look at this, purred Socks, jumping onto the desk and knocking over a lukewarm cup of coffee (which, fortunately, missed the trackpad). It's a knowledge graph. Itโ€™s entirely local. Itโ€™s... *perfect* for tracking the Red Dot.",
"Mittens walked across the keyboard, typing `[[The Great Nap]]`. Immediately, a new page was born. This was the power they had been looking for. No longer would their scheduling be chaotic. They would have structure. They would have **links**.",{gap="2rem"} )}

Option 3

  • you could also create a Library with .js to manipulate the DOM, and inject properly styled elements but that's above this proof of concept...

If you're looking for a js lib, there is a very flexible tool for window management:

Nice. I would love to see every "window"-ish thing generalized into something like this. Even the panels can, in fact be just "windows", with some "stickiness" to left/right/top/bottom and size/moving/resizing constraints. :slight_smile: But that would need some additional work on SilverBullets core side. @zef ?

Markdown is a relatively basic markup language, it is missing a lot of layout features you can find in things like latex or docx, but in return you get something that is easy to remember and is human readable even without being processed.

Markdown does support embedded html, so in theory you could just write html columns and put your markup in those. But silverbullet does not support this due to security concerns.
Mr red has the best practical advice for actually doing it with the currently available features, but like he said it doesn't seem like a great user experience

1 Like