Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions prototypes/scratchpad/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Scratchpad sidebar preview

Approved direction: one global sidebar scratchpad with a light paper tint and a
single kind of item. No workspace switch, document tabs, folders, rich-text
toolbar, item types or additional notes.

Open `index.html` in a browser. Type in the "Add something…" row and press Enter
to add an item. Click an item to edit it; Enter finishes editing. Ticking an item
moves it into a collapsed "Done" section at the bottom (unticking moves it back),
and "Clear" empties that section. Hover an item for its × to delete it; Backspace
on an empty item also deletes it, and items left empty are removed when you click
away. The paper uses a pale cream in light mode and a dark warm brown in dark
mode. It follows host theme changes (or system preference in a standalone
browser). The light/dark preview control is outside the proposed plugin UI.

This is an interactive design prototype, **not an installable or persistent
plugin**. Edits are kept only in memory and reset on reload. No storage calls,
browser-storage workaround, worker bridge or unsupported host API is used.

The sidebar surface is intentional. The user chose to wait for panel storage
support instead of using a file-backed custom editor. Once supported by Orca,
connect global plugin storage, add load/save/error states, and verify restart
persistence and concurrent panels before packaging it as a plugin. Workspace
identity is not needed for this global-only version.

Visual research: [Memos](https://github.com/usememos/memos) and
[PaperTodo](https://github.com/snownico0722/PaperTodo). This prototype is original;
it does not copy their code, artwork or assets. PaperTodo has a restrictive
noncommercial license, so its implementation should not be imported.
221 changes: 221 additions & 0 deletions prototypes/scratchpad/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scratchpad · sidebar preview</title>
<style>
:root { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --surface: #202123; --text: #e4e4e7; --muted: #8e8e96; --border: #38393c; --outside: #17181a; --paper: #3a3022; --fold: #5a4a33; --ink: #f0e6cf; --quiet-ink: #b0a284; --accent: #e6c884; --box: #9c8c6c; --checked: #d3b57b; --hover: #ffffff0a; --rule: #ffffff14; --focus: #e9c77e; --note-scheme: dark; }
:root[data-theme="light"] { --surface: #f5f5f3; --text: #353536; --muted: #77777a; --border: #dcdcd8; --outside: #e9e9e5; --paper: #fdf6dc; --fold: #ecdfb4; --ink: #3f3422; --quiet-ink: #8a7650; --accent: #8a6d3b; --box: #a8936a; --checked: #8a6d3b; --hover: #00000008; --rule: #00000010; --focus: #825c07; --note-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; padding: 24px 14px; background: var(--background, var(--outside)); color: var(--foreground, var(--text)); }
button { font: inherit; cursor: pointer; }
button:focus-visible, input:focus-visible, [contenteditable]:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }
.preview { max-width: 400px; margin: auto; }
.preview-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; color: var(--muted); font-size: 11px; }
.preview-label { letter-spacing: .12em; text-transform: uppercase; font-weight: 600; }
.theme { color: var(--foreground, var(--text)); background: transparent; border: 1px solid var(--border); padding: 6px 9px; border-radius: 6px; font-size: 11px; }
.panel { color-scheme: var(--note-scheme); background: var(--orca-background, var(--surface)); border: 1px solid var(--border); border-radius: 10px; padding: 14px 14px 12px; }
.paper { position: relative; background: var(--paper); color: var(--ink); min-height: 440px; padding: 22px 18px 16px; display: flex; flex-direction: column; }
.paper::before { content: ""; position: absolute; top: 0; right: 0; width: 26px; height: 26px; background: linear-gradient(to bottom left, var(--surface) 50%, var(--fold) 50%); }
.kicker { margin: 0; font-size: 10px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--quiet-ink); }
.title { margin: 6px 0 0; font-family: "Bradley Hand", "Segoe Print", "Comic Sans MS", cursive; font-weight: 400; font-size: 30px; line-height: 1.1; color: var(--accent); }
.squiggle { display: block; width: 108px; height: 8px; margin: 2px 0 18px; color: var(--accent); }
.rows { margin: 0; padding: 0; list-style: none; }
.row { display: flex; align-items: flex-start; gap: 10px; padding: 6px 10px; border-radius: 6px; }
.row:hover, .row:focus-within { background: var(--hover); }
.tick { appearance: none; flex: 0 0 18px; width: 18px; height: 18px; margin: 2px 0 0; border: 1.5px solid var(--box); border-radius: 4px; background: transparent; cursor: pointer; position: relative; }
.tick:checked { background: var(--checked); border-color: var(--checked); }
.tick:checked::after { content: ""; position: absolute; left: 5px; top: 1px; width: 4px; height: 9px; border: solid var(--paper); border-width: 0 2px 2px 0; transform: rotate(45deg); }
.line { flex: 1; min-width: 0; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; white-space: pre-wrap; outline: none; }
.remove { flex: 0 0 auto; border: 0; background: transparent; color: var(--quiet-ink); padding: 0 2px; font-size: 16px; line-height: 1.3; opacity: 0; }
.row:hover .remove, .row:focus-within .remove, .remove:focus-visible { opacity: 1; }
.remove:hover { color: var(--ink); }
.composer .tick-ghost { flex: 0 0 18px; height: 18px; margin-top: 2px; border: 1.5px dashed var(--box); border-radius: 4px; opacity: .6; }
.composer input { flex: 1; min-width: 0; border: 0; background: transparent; color: var(--ink); font: inherit; font-size: 14px; line-height: 1.55; padding: 0; outline: none; }
.composer input::placeholder { color: var(--quiet-ink); }
.done-section { margin-top: 18px; border-top: 1px solid var(--rule); padding-top: 10px; }
.done-section[hidden] { display: none; }
.done-head { display: flex; justify-content: space-between; align-items: center; padding: 0 10px; }
.toggle, .clear { border: 0; background: transparent; color: var(--quiet-ink); font-size: 11px; font-weight: 600; letter-spacing: .04em; padding: 4px 0; }
.toggle:hover, .clear:hover { color: var(--ink); }
.toggle::before { content: "▸"; display: inline-block; margin-right: 6px; transition: transform .15s; }
.toggle[aria-expanded="true"]::before { transform: rotate(90deg); }
.done .line { color: var(--quiet-ink); text-decoration: line-through; text-decoration-thickness: 1px; }
.legend { display: flex; gap: 14px; margin: 18px 10px 0; padding-top: 12px; border-top: 1px solid var(--rule); font-size: 11px; color: var(--quiet-ink); }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend svg { width: 12px; height: 12px; }
.paper-foot { margin-top: auto; padding: 18px 10px 0; display: flex; justify-content: space-between; align-items: flex-end; font-size: 11px; color: var(--quiet-ink); }
.paper-foot svg { width: 22px; height: 22px; color: var(--quiet-ink); }
.hint { margin: 13px 6px 0; text-align: center; font-size: 11px; line-height: 1.6; color: var(--muted); }
.done-section[hidden], .rows[hidden] { display: none; }
.disclosure { color: var(--foreground, var(--muted)); margin: 13px 6px 0; font-size: 11px; line-height: 1.6; }
@media (prefers-reduced-motion: reduce) { .toggle::before { transition: none; } }
</style>
</head>
<body>
<main class="preview">
<div class="preview-bar">
<span class="preview-label">Sidebar design preview</span>
<button class="theme" type="button">Light preview</button>
</div>
<section class="panel" aria-label="Scratchpad sidebar">
<div class="paper">
<ul class="rows open" aria-label="Open items"></ul>
<div class="row composer">
<span class="tick-ghost" aria-hidden="true"></span>
<input type="text" placeholder="Add something…" aria-label="Add an item">
</div>
<section class="done-section" hidden>
<div class="done-head">
<button class="toggle" type="button" aria-expanded="false" aria-controls="done-list">Done · 0</button>
<button class="clear" type="button">Clear</button>
</div>
<ul class="rows done" id="done-list" aria-label="Done items" hidden></ul>
</section>
<div class="paper-foot">
<span class="count">2 little things left</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M8 14.5c1 1.4 2.4 2 4 2s3-.6 4-2"/><path d="M9 9.5h.01M15 9.5h.01" stroke-width="2"/></svg>
</div>
</div>
<p class="hint">Click to edit · Enter to finish · Shift+Enter for a new line<br>Backspace on an empty item to remove it</p>
</section>
<p class="disclosure"><strong>Preview only.</strong> Edits reset when reloaded. Saving is waiting on Orca’s panel storage API.</p>
</main>
<script>
(() => {
'use strict';
// Deliberately in-memory. Do not imply durability or use browser storage as
// a substitute for the pending, supported Orca panel storage contract.
const openList = document.querySelector('.rows.open');
const doneList = document.querySelector('.rows.done');
const doneSection = document.querySelector('.done-section');
const toggle = document.querySelector('.toggle');
const composer = document.querySelector('.composer input');
const count = document.querySelector('.count');

function updateDone() {
const n = doneList.children.length;
doneSection.hidden = n === 0;
if (n === 0) {
toggle.setAttribute('aria-expanded', 'false');
doneList.hidden = true;
}
toggle.textContent = `Done · ${n}`;
const open = openList.children.length;
count.textContent = `${open} little ${open === 1 ? 'thing' : 'things'} left`;
}
function focusEnd(el) {
el.focus();
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function makeRow(text, done = false) {
const row = document.createElement('li');
row.className = 'row';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'tick';
checkbox.checked = done;
const line = document.createElement('span');
line.className = 'line';
line.contentEditable = 'plaintext-only';
line.setAttribute('role', 'textbox');
line.setAttribute('aria-label', 'Item text');
line.textContent = text;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'remove';
remove.textContent = '×';
const label = () => {
const name = line.textContent.trim() || 'item';
checkbox.setAttribute('aria-label', `${checkbox.checked ? 'Mark open' : 'Mark done'}: ${name}`);
remove.setAttribute('aria-label', `Delete: ${name}`);
};
label();
line.addEventListener('input', label);
checkbox.addEventListener('change', () => {
(checkbox.checked ? doneList : openList)[checkbox.checked ? 'prepend' : 'append'](row);
label();
updateDone();
if (checkbox.checked && doneList.hidden) composer.focus();
else checkbox.focus();
});
remove.addEventListener('click', () => { row.remove(); updateDone(); composer.focus(); });
line.addEventListener('keydown', event => {
if (event.isComposing) return;
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
line.blur();
composer.focus();
} else if (event.key === 'Backspace' && !line.textContent) {
event.preventDefault();
const prev = row.previousElementSibling?.querySelector('.line');
row.remove();
updateDone();
prev ? focusEnd(prev) : composer.focus();
}
});
line.addEventListener('blur', () => { if (!line.textContent.trim()) { row.remove(); updateDone(); } });
row.append(checkbox, line, remove);
return row;
}

composer.addEventListener('keydown', event => {
if (event.isComposing) return;
if (event.key === 'Enter' && composer.value.trim()) {
openList.append(makeRow(composer.value.trim()));
composer.value = '';
updateDone();
} else if (event.key === 'Backspace' && !composer.value && openList.lastElementChild) {
event.preventDefault();
focusEnd(openList.lastElementChild.querySelector('.line'));
}
});
toggle.addEventListener('click', () => {
const open = toggle.getAttribute('aria-expanded') !== 'true';
toggle.setAttribute('aria-expanded', String(open));
doneList.hidden = !open;
});
document.querySelector('.clear').addEventListener('click', () => {
doneList.replaceChildren();
updateDone();
composer.focus();
});

openList.append(makeRow('Try the scratchpad'), makeRow('Keep it small. Make it a little fun.'), makeRow('Leave something for future me'));
doneList.append(makeRow('Make a little space for ideas', true));
updateDone();

const theme = document.querySelector('.theme');
const root = document.documentElement;
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(mode) {
root.dataset.theme = mode;
const next = mode === 'light' ? 'dark' : 'light';
theme.textContent = `${next === 'light' ? 'Light' : 'Dark'} preview`;
theme.setAttribute('aria-label', `Switch to ${next} preview`);
}
function followHostTheme() {
const scheme = getComputedStyle(root).colorScheme;
applyTheme(root.classList.contains('light') ? 'light'
: root.classList.contains('dark') ? 'dark'
: scheme === 'light' || scheme === 'dark' ? scheme
: systemTheme.matches ? 'dark' : 'light');
}
theme.addEventListener('click', () => applyTheme(root.dataset.theme === 'light' ? 'dark' : 'light'));
systemTheme.addEventListener('change', followHostTheme);
window.orca?.on?.('theme', ({ colorScheme }) => {
if (colorScheme === 'light' || colorScheme === 'dark') applyTheme(colorScheme);
else followHostTheme();
});
new MutationObserver(followHostTheme).observe(root, { attributes: true, attributeFilter: ['class', 'style'] });
followHostTheme();
})();
</script>
</body>
</html>
Loading