Skip to main content
Rundock UI is Rundock’s own component library, handed to your extension. Your view calls Rundock.ui.<component>(options), gets an element back, and puts it on the page. Buttons, fields, tabs, tables, boards and the rest are Rundock’s; what you draw inside a canvas is yours. Rundock puts the library into every extension view, with its stylesheet and the colours of the theme showing, before your script runs. You never bundle a copy, so your view always matches the Rundock it is running in. When Rundock restyles a button, your buttons change with it, and you publish nothing.

See every component

A running Rundock shows every component in every state, in both themes, at /rundock-ui/gallery. In browser mode that is:
Each theme on that page is a real extension view built by Rundock the way it builds yours, so what you see there is exactly what your view gets. Keep it open while you build.

Use it

The library is on window.Rundock.ui before your entry script starts, so you can call it straight away:
  • Every factory takes one options object and returns an element. You compose them like any other DOM node.
  • Labels are always text. A label containing <b> shows <b>; nothing you pass is parsed as markup.
  • A wrong option is refused by name. An unknown variant or tone, or a callback that is not a function, throws a TypeError naming the component and the option, for example Rundock.ui.button: variant must be one of primary, secondary, danger, danger-confirm. An uncaught error ends your view and names the reason, so you find out while building.
  • Your styles win. Components are styled by classes that start with rui-, and your own stylesheets are inlined after Rundock UI’s, so you can override any of them. Use Rundock UI for controls and containers, and keep your own CSS to layout.
For a whole extension built this way, see Build an extension.

Components

Twenty-two components, from twenty-three factories: an empty state and a loading placeholder are one component with a factory each. Every one is shown, in every state, in the gallery. Rundock.ui.version says which version of the library is running. The full options, defaults and keyboard behaviour of every component are in docs/RUNDOCK-UI.md in the Rundock repository.

Versions

Rundock.ui.version is a string, "MAJOR.MINOR". This release of Rundock provides "1.0".
  • A minor version adds. A new component, a new option or a new state.
  • A major version changes what an existing call does or accepts.
  • A change of look is neither. Rundock puts the library into your view itself, so a restyle reaches every extension at once and asks nothing of you.
Declare the version you built against in your manifest, as extension.rundockUi:
The rule: an extension built against X.Y installs on a Rundock that provides X.Z, where Z is Y or greater, and on nothing else. An older minor might lack something you call, and another major might answer a call differently. Rundock refuses any other install and says why, for example: “the extension was built against Rundock UI 1.3, newer than the 1.0 this Rundock provides: update Rundock to install it.” rundockUi is optional. An extension that declares nothing installs as before and still gets the library, with no check. A value that is not MAJOR.MINOR, such as "1" or " 1.0", is refused. To use a newer component where it exists and fall back where it does not, test for it: typeof Rundock.ui.meter === 'function', or read Rundock.ui.version.

Tables

table takes columns, an array of { key, label, numeric, align, render, format, width, minWidth, widest, grow, resizable, edit }, and rows, an array of objects. A numeric column is right-aligned with tabular figures. caption is read by assistive technology and not shown. There is no sorting in this version.

Widths

A table measures each column once, the first time it is laid out: its natural width is its header and its widest value on one line. It then fixes the widths, so editing a cell never shifts a column.
  • Every column takes its natural width, except text wider than 280px, which is capped there. A value cut off by the cap ends in an ellipsis, with its whole text as the cell’s title. A numeric column, a checkbox or a row menu is never capped, and no column is narrower than its header.
  • The main column takes the spare room. When the table is wider than its columns, the widest text column takes all the spare room and every other column keeps its natural width, so a short column stays tight and a row menu sits at the right edge. A numeric column, a control such as a checkbox or a row menu, and a column with a fixed width are never chosen.
  • grow: true chooses the main column yourself, such as a notes column, instead of leaving it to the widest.
  • When no column can take it, for example when every column is fixed, numeric or a control, or the main column has been resized, the rest goes to an empty filler column after the last, so rows still run the full width. The filler has no header, is hidden from assistive technology and is never focusable.
  • A table wider than its container keeps its widths and scrolls sideways inside its own wrapper, never the page.
A column that may show something wider than its first rows do, such as “Not priced”, names it in widest: text or an element, or an array of them, measured with the cells. minWidth sets a floor in pixels. width fixes a column, as pixels or a CSS length such as "96px", "20%" or "12ch", and always wins. When the container’s width changes, the spare room is given out again, but never while a cell is being edited.

Resizing

resizable: true on the table gives every column a resize handle on its header’s right edge, and resizable on a column turns it on or off for that column alone. A handle is a focusable separator named for its column, such as “Resize the Quantity column”. Drag it, or focus it and press the arrow keys, 8px a step or 32px with Shift. Enter or a double-click puts the column back to the default rule. A column never goes below its header or its minWidth. onResize({ key, width }) reports each resize, and width: null after a reset. A view that keeps widths passes each kept width back as that column’s width when it redraws the table. Never write widths into the person’s notes: a width is a view preference, not their data. Kept widths. stateKey: '<name>' on a resizable table keeps its widths across a reload and a return to the note, with no code of yours: the table reads and writes them in the view’s own state, under the key rui.table.<name>. A kept width wins over the default rule until the person resets that column, and never falls below the column’s header or minWidth. stateKey is 1 to 54 letters, digits, ., _, : or -; give each table in a view its own. Where there is no view state, such as the gallery, a table with stateKey behaves exactly as one without it. Like all view state, kept widths are per machine, and a renamed or moved note starts without them.

Format

format(value, row) is what a cell shows, as text or an element. A column takes format or render, not both. An editable column must use format, because the table redraws the cell from its value after an edit.

Editing

A column with edit can be changed in place; one without is read-only.
  • edit.type is number (the default), text, select or checkbox. min and max bound a number. options, required for select, are strings or { value, label }. label(row) names the row for assistive technology. when(row) returning false leaves that one cell read-only.
  • Keys. Click, tap, Enter or F2 opens the editor over the cell. Enter commits and Escape cancels. Tab commits and opens the next editable cell, Shift+Tab the one before. Leaving the editor commits. There is no arrow-key movement between cells.
  • A number is digits with an optional - and one ., with , read as grouping. Anything else, or a number outside min and max, is refused by the table with the range in words, such as “Enter a number of 0 or more.”, and never reaches onEdit. An unchanged value closes without asking.
  • onEdit({ row, key, value, previous }) decides. Return true to accept. Return a message to refuse: the value reverts, and the message shows on a line of its own directly under the row, spanning the table and naming the column, such as “Quantity: More than this account holds.”, and focus stays in the editor. Return a promise to hold the cell while you save: the cell shows “Saving…” until the promise settles as true or a message. Anything else, a throw or a promise that fails, refuses with “This change was not saved.”, so the table never shows a value you did not confirm.
  • A checkbox shows as a checkbox all the time and toggles in place with one click or Space, asking onEdit with true or false. A refusal unticks it again.
  • The table never writes anything, neither the file nor your rows. Updating your own data, and anything derived from it, is yours to do in onEdit. With no onEdit, nothing can be edited: every cell reads as text, and a checkbox is shown disabled.

View state

Rundock.viewState keeps your view’s own preferences for the note it is showing, such as a chosen tab or the widths someone dragged, so you never write them into the person’s note.
  • get(key) returns the value kept under key, or undefined. It works from your entry’s first line: the state is in the frame before your script runs.
  • set(key, value) changes the view’s copy at once and asks Rundock to keep the whole state. set(key, undefined) removes the entry.
  • A key is 1 to 64 letters, digits, ., _, : or -. Any other key throws a TypeError and nothing is sent. Keys starting rui. belong to Rundock UI, so do not use that prefix for your own.
  • The limits are those of a view’s own state: plain JSON, 64 KB per note, kept per machine, and refused from an embedded view. A refusal arrives as refused with of: 'setState', and your copy stays as you set it, so the view keeps working for the session. See A view’s own state.
A fenced-block extension, which belongs to no one note, has no Rundock.viewState.

Where a view sits: full bleed

A view opened on its own is the pane, not a box inside it. The frame fills the file pane edge to edge and top to bottom, and paints the pane’s own colour in both themes, with no border or rounded corners. Your page’s body carries the same padding a note has, so your view’s first line starts exactly where a note’s first line of text does. Do not add an outer padding or a background of your own. Rundock has already placed your view; extra padding makes it look inset. There are two variations, each a class on your body:
  • An edge-to-edge canvas. Add rundock-full-bleed to the body, and the padding goes to zero:
    Setting body { padding: ... } in your own stylesheet works too, since it comes after Rundock’s.
  • Embedded in another note. When your view is shown inside another note, Rundock adds rundock-embedded to the body. The panel around it already pads it, so the body has no padding and paints the panel’s colour. You need do nothing, but your view should read well at a third of the pane’s width. See Show up inside other notes.

What the library can and cannot do

Rundock UI runs inside your view’s frame, as your code does, with no more reach than your code has. It builds elements and listens to them. It sends no message to Rundock, reads nothing outside the frame and fetches nothing. The frame’s sandbox, its content security policy and the closed list of messages in the extension host contract are exactly the same with it as without it. In the light theme, your body also carries the class light, the same class Rundock’s own page carries, so a rule written body.light ... behaves in your view as it does in Rundock. The theme in init says the same thing. When the theme changes while your view is open, a view whose ready says handles: ['theme'] is restyled in place: every component follows, and the view keeps its state, including edits not yet saved. A view that does not say so is rebuilt and starts again from the file. A view that edits should say it. See Theme changes.

Design rules the components follow

These are the decisions behind the look. If you draw your own content in a canvas, follow them too and it will sit comfortably beside the components.
  • Two oranges, each for one kind of shape. Action orange, --accent-action, fills a shape that carries white text or an icon: the primary button, the accent chip and the send button. Control orange, --accent-control, fills a bare shape: a checked checkbox, a toggle that is on, a slider’s fill and thumb, the selected option’s dot and a meter’s fill. Text on a fill needs more contrast than a shape against its background does, which is why there are two.
  • The brand orange, --accent, is never a fill. It is for outlines, focus rings, hover and drag highlights, and links.
  • Danger differs by form as well as colour. Action orange and red are too close in brightness to tell apart by colour alone. A button that starts a destructive action is a red outline; only the final destructive action is solid red.
  • Status is a solid fill with dark or white text, never coloured text on a pale tint of the same colour, which is hard to read in the light theme.
  • A control’s edge is --text-3, which stands out clearly against the surfaces controls sit on. --border is for decorative dividers only.
  • Named, not only coloured. Every state a component shows by colour is also carried by a word, a role, a shape or a mark.
  • Motion stops when reduced motion is on, and everything that waits breathes on one two-second rhythm.

Where to next

Build an extension

A complete extension built on Rundock UI.

What an extension cannot do

The sandbox your view runs in.