Bear /now dashboard plugin
I’ve always been terrible at updating my /now page. I forget about it.
One of the reasons is that it moves further and further down in my dashboard. I create themes, add-ons, and other little projects. I have to scroll to find it in my digital jungle.
So, in an attempt to try to keep my now page better updated, I set out to create a plugin to help me with that.

After installing it, you’ll get a new “Now” menu option in the Bear dashboard. Clicking it takes you straight to the edit screen. Easy peasy!

Here you will also find a new “Now cards” option. Hit it and a card opens with options to easily add what you’re currently reading, watching, listening to, or playing.

Will this help me get better at keeping my page fresh? I don’t know, but at least I’ve tried my best...
Installation
Requires JavaScript, available with Bear Blog subscription.
- Copy the script below.
- Go to Customise dashboard.
- Paste it under “Dashboard footer content”.
- Save. Enjoy.
If your now page isn’t called “Now”, change NOW_TITLE at the top of the script.
Script
<script>
/* Now cards · v1.0 · Bear dashboard plugin by Robert Birming */
(() => {
'use strict';
const NOW_TITLE = 'Now'; // your now page's title, if it isn't "Now"
const KEY = 'now-cards-edit-url';
// Line format: *Title* + pre + extra + post (extra is optional)
const CARDS = {
book: { label: 'Book', emoji: '📖', statuses: ['Currently reading', 'Finished reading'], title: 'Title', extra: 'Author', pre: ' by ' },
movie: { label: 'Movie', emoji: '🎬', statuses: ['Just watched', 'Want to watch'], title: 'Title', extra: 'Year', pre: ' (', post: ')', numeric: true, max: 4 },
show: { label: 'Show', emoji: '📺', statuses: ['Currently watching', 'Finished watching'], title: 'Title', extra: 'Season', pre: ', season ', numeric: true, max: 3 },
game: { label: 'Game', emoji: '🎮', statuses: ['Currently playing', 'Finished playing'], title: 'Title', extra: 'Platform', pre: ' on ' },
music: { label: 'Music', emoji: '🎧', statuses: ['Listening to', 'On repeat'], title: 'Album or track', extra: 'Artist', pre: ' by ' },
};
/* ---------- helpers ---------- */
const lc = s => s.trim().toLowerCase();
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const isNowTitle = t => /(^|[\s/])now( page)?$/.test(lc(t));
const store = {
get() { try { return localStorage.getItem(KEY); } catch { return null; } },
set(v) { try { localStorage.setItem(KEY, v); } catch {} },
};
// A card = its heading line (bold or not) plus the lines right after it,
// up to a blank line, a Markdown heading, or the next card's heading
const ALL_EMOJI = Object.values(CARDS).map(c => esc(c.emoji)).join('|');
const cardRe = type => {
const c = CARDS[type];
return new RegExp(
`^[ \\t]*(?:\\*\\*)?${esc(c.emoji)} (?:${c.statuses.map(esc).join('|')})(?:\\*\\*)?[ \\t]*$` +
`(?:\\n(?![ \\t]*(?:#|(?:\\*\\*)?(?:${ALL_EMOJI}) ))[ \\t]*\\S.*)*`,
'm'
);
};
function readCard(text, type) {
const m = text.match(cardRe(type));
if (!m) return null;
const c = CARDS[type];
const lines = m[0].split('\n').map(s => s.trim());
const pre = c.pre || '';
const post = c.post || '';
const line = lines[1] || '';
let title = line;
let extra = '';
const it = line.match(/^\*([^*]+)\*(.*)$/);
if (it) {
title = it[1];
const rest = it[2];
if (rest.startsWith(pre) && rest.endsWith(post) && rest.length > pre.length + post.length) {
extra = rest.slice(pre.length, rest.length - post.length);
}
} else if (line.includes(' — ')) {
// older hand-written style: Title — Creator
const i = line.lastIndexOf(' — ');
title = line.slice(0, i);
extra = line.slice(i + 3);
}
return {
status: c.statuses.find(s => lines[0].includes(s)) || c.statuses[0],
title,
extra,
note: lines.slice(2).join(' '),
};
}
function renderCard(type, v) {
const c = CARDS[type];
const line = `*${v.title}*` + (v.extra ? `${c.pre || ''}${v.extra}${c.post || ''}` : '');
return [`**${c.emoji} ${v.status}**`, line, v.note].filter(Boolean).join('\n');
}
/* ---------- 1. "Now" link in the main nav ---------- */
function addNavLink() {
const nav = document.querySelector('header nav span');
const home = nav && nav.querySelector('a[href$="/dashboard/"]');
if (!home) return;
const saved = store.get();
const base = home.getAttribute('href');
const pages = nav.querySelector(`a[href="${base}pages/"]`);
const link = document.createElement('a');
link.textContent = 'Now';
link.href = saved || base + 'pages/';
(pages || nav.lastElementChild).after(' ', link);
if (saved) return;
fetch(base + 'pages/', { credentials: 'same-origin' })
.then(r => (r.ok ? r.text() : Promise.reject()))
.then(html => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const links = [...doc.querySelectorAll('main a')];
const match =
links.find(a => lc(a.textContent) === lc(NOW_TITLE)) ||
links.find(a => isNowTitle(a.textContent));
if (match) {
const url = match.getAttribute('href');
store.set(url);
link.href = url;
}
})
.catch(() => {});
}
/* ---------- 2. Now cards in the now page editor ---------- */
function initEditor() {
const body =
document.querySelector('textarea[name="body_content"]') ||
document.querySelector('main form textarea');
if (!body) return;
const headerText =
document.querySelector('[name="header_content"]')?.value ||
document.querySelector('.editor-header')?.innerText ||
'';
const byHeader = /^link:\s*\/?now\/?\s*$/im.test(headerText);
if (!byHeader && store.get() !== location.pathname) return;
// Refresh the saved URL whenever the header confirms this is the now page,
// so a recreated page doesn't leave the nav link pointing at the old one
if (byHeader) store.set(location.pathname);
// Only use the cursor position if you've actually clicked, typed or pasted in the editor
// (not on focus: if Bear focuses the editor on load, the cursor would sit at the very top)
let touched = false;
['click', 'keyup', 'input'].forEach(e => body.addEventListener(e, () => { touched = true; }));
// Trigger, first in the bottom row
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'link-button nc-trigger';
trigger.textContent = 'Now cards';
const insertMedia = [...document.querySelectorAll('a, button')]
.find(el => lc(el.textContent) === 'insert media');
if (insertMedia) insertMedia.before(trigger, ' | ');
else (document.querySelector('.helptext') || body.parentElement).append(' | ', trigger);
// Dialog
const dlg = document.createElement('dialog');
dlg.className = 'now-cards';
dlg.setAttribute('aria-labelledby', 'nc-heading');
dlg.innerHTML = `
<form method="dialog">
<div class="nc-head">
<h2 id="nc-heading">Now cards</h2>
<button type="button" class="nc-close" aria-label="Close">✕</button>
</div>
<div class="nc-types" role="group" aria-label="Card type"></div>
<div class="nc-fields"></div>
<div>
<p class="nc-label">Preview</p>
<pre class="nc-preview" aria-live="polite"></pre>
</div>
<div class="nc-actions">
<button type="button" class="nc-close">Cancel</button>
<button value="apply">Apply</button>
</div>
</form>`;
document.body.append(dlg);
const form = dlg.querySelector('form');
const types = dlg.querySelector('.nc-types');
const fields = dlg.querySelector('.nc-fields');
const preview = dlg.querySelector('.nc-preview');
let current = null;
let pending = null;
const val = name => form.querySelector(`[name="${name}"]`).value.trim().replace(/\s+/g, ' ');
const values = () => ({
status: form.querySelector('[name="nc-status"]:checked')?.value || CARDS[current].statuses[0],
title: val('nc-title'),
extra: val('nc-extra'),
note: val('nc-note'),
});
function update() {
const v = values();
preview.textContent = v.title ? renderCard(current, v) : `${CARDS[current].title} is needed`;
preview.classList.toggle('muted', !v.title);
}
function select(type) {
current = type;
types.querySelectorAll('[data-type]').forEach(b =>
b.setAttribute('aria-pressed', String(b.dataset.type === type)));
const c = CARDS[type];
const v = readCard(body.value, type) || { status: c.statuses[0], title: '', extra: '', note: '' };
const numeric = c.numeric ? ` inputmode="numeric" pattern="[0-9]*" maxlength="${c.max}"` : '';
fields.innerHTML =
`<fieldset><legend>Status</legend>${c.statuses.map(s =>
`<label><input type="radio" name="nc-status" value="${s}"> ${s}</label>`).join('')}</fieldset>` +
`<label>${c.title}<input name="nc-title" required autocomplete="off"></label>` +
`<label>${c.extra} <span class="muted">(optional)</span><input name="nc-extra" autocomplete="off"${numeric}></label>` +
`<label>Note <span class="muted">(optional)</span><input name="nc-note" autocomplete="off" maxlength="200"></label>`;
const radio = [...fields.querySelectorAll('[name="nc-status"]')].find(r => r.value === v.status);
(radio || fields.querySelector('[name="nc-status"]')).checked = true;
fields.querySelector('[name="nc-title"]').value = v.title;
fields.querySelector('[name="nc-extra"]').value = v.extra;
fields.querySelector('[name="nc-note"]').value = v.note;
update();
}
trigger.addEventListener('click', () => {
const text = body.value;
types.innerHTML = Object.entries(CARDS).map(([k, c]) => {
const onPage = cardRe(k).test(text);
return `<button type="button" data-type="${k}"${onPage ? ' title="On the page"' : ''}>${c.emoji} ${c.label}${onPage ? ' •' : ''}</button>`;
}).join('');
const first = Object.keys(CARDS).find(k => cardRe(k).test(text)) || 'book';
select(current || first);
dlg.returnValue = '';
dlg.showModal();
fields.querySelector('[name="nc-title"]').focus();
});
types.addEventListener('click', e => {
const b = e.target.closest('[data-type]');
if (b) select(b.dataset.type);
});
fields.addEventListener('input', update);
dlg.querySelectorAll('.nc-close').forEach(b => b.addEventListener('click', () => dlg.close()));
form.addEventListener('submit', () => {
const v = values();
if (!v.title) { pending = null; return; }
const card = renderCard(current, v);
const text = body.value;
const m = text.match(cardRe(current));
if (m) {
pending = { start: m.index, end: m.index + m[0].length, str: card, offset: 0 };
} else {
const pos = touched ? body.selectionStart : text.length;
const before = text.slice(0, pos);
const after = text.slice(pos);
const pre = !before || before.endsWith('\n\n') ? '' : before.endsWith('\n') ? '\n' : '\n\n';
const post = !after ? '\n' : after.startsWith('\n\n') ? '' : after.startsWith('\n') ? '\n' : '\n\n';
pending = { start: pos, end: pos, str: pre + card + post, offset: pre.length };
}
});
// Write after the dialog has closed, so the editor can take focus (and undo works)
dlg.addEventListener('close', () => {
if (dlg.returnValue !== 'apply' || !pending) { pending = null; return; }
const { start, end, str, offset } = pending;
pending = null;
body.focus();
body.setSelectionRange(start, end);
if (!document.execCommand('insertText', false, str)) {
body.setRangeText(str, start, end, 'end');
body.dispatchEvent(new Event('input', { bubbles: true }));
}
const cardStart = start + offset;
body.setSelectionRange(cardStart, cardStart);
body.blur();
body.focus();
});
// Styles, using Bear's own dashboard variables
const style = document.createElement('style');
style.textContent = `
button.nc-trigger.link-button:not(:disabled):hover { background: none; text-decoration: underline; }
dialog.now-cards { width: min(30rem, calc(100% - 2rem)); padding: 0; border: 1px solid var(--border); border-radius: var(--radius, 0); background: var(--background-color); color: var(--text-color); }
dialog.now-cards::backdrop { background: rgb(0 0 0 / 0.45); }
.now-cards form { display: grid; gap: 1rem; padding: 1rem 1.25rem 1.25rem; }
.now-cards .nc-head { display: flex; justify-content: space-between; align-items: center; }
.now-cards h2 { margin: 0; font-size: 1.15em; }
.now-cards .nc-types { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.now-cards .nc-types [aria-pressed="true"] { border-color: var(--link-color); color: var(--link-color); }
.now-cards .nc-fields { display: grid; gap: 0.75rem; }
.now-cards .nc-fields > label { display: grid; gap: 0.25rem; }
.now-cards .nc-fields .muted { font-weight: normal; font-size: small; }
.now-cards fieldset { border: 0; padding: 0; margin: 0; }
.now-cards legend { font-weight: bold; padding: 0; margin-bottom: 0.25rem; }
.now-cards fieldset label { display: inline-flex; align-items: center; margin-right: 1rem; font-weight: normal; }
.now-cards input:not([type="radio"]) { width: 100%; box-sizing: border-box; padding: 6px 10px; font-size: 16px; }
.now-cards .nc-label { margin: 0 0 0.25rem; font-weight: bold; }
.now-cards .nc-preview { margin: 0; padding: 0.75rem; background: var(--surface); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14px; }
.now-cards .nc-actions { display: flex; justify-content: flex-end; gap: 0.5rem; }
`;
document.head.append(style);
}
addNavLink();
initEditor();
})();
</script>
Now go update your now page.
Want more? Check out the full Bear Blog library.