rewritable 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/seeds/rewritable.html +183 -0
package/package.json
CHANGED
package/seeds/rewritable.html
CHANGED
|
@@ -88,6 +88,17 @@ body{font-family:var(--font-ui);background:var(--bg);color:var(--text);min-heigh
|
|
|
88
88
|
#rwa-info-panel .rwa-info-foot{margin-top:12px;padding-top:10px;border-top:1px solid var(--gray-100);font-size:11px;color:var(--gray-500);line-height:1.5;}
|
|
89
89
|
#rwa-info-panel strong{color:var(--gray-900);font-weight:600;}
|
|
90
90
|
#rwa-info-panel code{font-family:var(--font-mono);font-size:11px;background:var(--gray-50);padding:1px 5px;border-radius:4px;color:var(--gray-700);}
|
|
91
|
+
#rwa-share-panel{position:fixed;top:50px;right:12px;background:var(--white);border:1px solid var(--gray-200);border-radius:var(--radius-sm);padding:18px 20px;display:none;max-width:340px;z-index:999;box-shadow:0 4px 16px rgba(0,0,0,0.08);font-family:var(--font-ui);font-size:13px;line-height:1.55;color:var(--gray-700);}
|
|
92
|
+
#rwa-share-panel.open{display:block;}
|
|
93
|
+
#rwa-share-panel h4{margin:0 0 6px;font-size:15px;color:var(--gray-900);font-weight:600;}
|
|
94
|
+
#rwa-share-panel p{margin:0 0 10px;}
|
|
95
|
+
.rwa-share-url{font-family:var(--font-mono);font-size:11px;background:var(--gray-50);border:1px solid var(--gray-100);border-radius:6px;padding:6px 8px;margin:0 0 8px;cursor:pointer;word-break:break-all;color:var(--gray-700);}
|
|
96
|
+
.rwa-share-fresh{font-family:var(--font-mono);font-size:10px;color:var(--gray-500);margin-bottom:10px;}
|
|
97
|
+
.rwa-share-msg{font-size:12px;color:var(--gray-600);background:var(--gray-50);border-radius:6px;padding:6px 8px;margin-bottom:8px;}
|
|
98
|
+
.rwa-share-actions{display:flex;gap:6px;flex-wrap:wrap;}
|
|
99
|
+
.rwa-share-actions button{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--gray-300);border-radius:8px;background:var(--white);color:var(--gray-900);cursor:pointer;}
|
|
100
|
+
.rwa-share-actions button:disabled{opacity:.5;cursor:default;}
|
|
101
|
+
#rwa-share-create,#rwa-share-update{background:var(--gray-900);color:var(--white);border-color:var(--gray-900);}
|
|
91
102
|
.rwa-set-hint code{background:var(--gray-100);padding:1px 4px;border-radius:3px;font-size:10px;}
|
|
92
103
|
.rwa-set-hint.ok{color:#15803d;}
|
|
93
104
|
.rwa-set-hint.err{color:#b91c1c;}
|
|
@@ -376,6 +387,13 @@ const RWA = {
|
|
|
376
387
|
// for users with a Claude subscription; the price is a per-call subprocess
|
|
377
388
|
// (~5-10s startup) and a single-shot agent loop (no mid-stream tool_calls).
|
|
378
389
|
BRIDGE_URL:'http://127.0.0.1:8765/run',
|
|
390
|
+
// Connected-share service (the ↗ panel): a stable share URL the user
|
|
391
|
+
// explicitly publishes versions to. Network I/O happens ONLY on share
|
|
392
|
+
// gestures — never at boot, never on ⌘S — so offline-first holds. Dev /
|
|
393
|
+
// self-hosted services override via sessionStorage rwa_share_base.
|
|
394
|
+
// Design: docs/plans/2026-06-11-save-affordance-framings.md §7c.
|
|
395
|
+
SHARE_BASE:'https://rewritable.ikangai.com',
|
|
396
|
+
K_SHARE_BASE:'rwa_share_base',
|
|
379
397
|
};
|
|
380
398
|
|
|
381
399
|
// `rwa new -o` / `rwa import -o` can pass first-paint configuration via URL
|
|
@@ -1095,6 +1113,7 @@ function buildUI() {
|
|
|
1095
1113
|
<button class="rwa-st-btn" id="rwa-st-info" title="what is this?" aria-label="what is this?">ⓘ</button>
|
|
1096
1114
|
<button class="rwa-st-btn" id="rwa-st-cog">⚙</button>
|
|
1097
1115
|
<button class="rwa-st-btn" id="rwa-st-skin" title="skins — pick a look" aria-label="skins">✦</button>
|
|
1116
|
+
<button class="rwa-st-btn" id="rwa-st-share" title="share at a link" aria-label="share at a link">↗</button>
|
|
1098
1117
|
<button class="rwa-st-btn pri" id="rwa-st-commit">⌘S</button>
|
|
1099
1118
|
</div>
|
|
1100
1119
|
<div id="rwa-set-panel">
|
|
@@ -1108,6 +1127,7 @@ function buildUI() {
|
|
|
1108
1127
|
</div>
|
|
1109
1128
|
<div id="rwa-info-panel"></div>
|
|
1110
1129
|
<div id="rwa-skin-panel"></div>
|
|
1130
|
+
<div id="rwa-share-panel"></div>
|
|
1111
1131
|
<div id="rwa-pal">
|
|
1112
1132
|
<div id="rwa-pal-box">
|
|
1113
1133
|
<div class="rwa-pal-top">
|
|
@@ -1289,6 +1309,7 @@ function buildUI() {
|
|
|
1289
1309
|
document.getElementById('rwa-st-cog').onclick = () => {
|
|
1290
1310
|
document.getElementById('rwa-info-panel').classList.remove('open'); // panels are mutually exclusive
|
|
1291
1311
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1312
|
+
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1292
1313
|
document.getElementById('rwa-set-panel').classList.toggle('open');
|
|
1293
1314
|
};
|
|
1294
1315
|
// ⓘ "what is this?" — render the rwa-identity/1 bundle as prose on open so it
|
|
@@ -1297,6 +1318,7 @@ function buildUI() {
|
|
|
1297
1318
|
document.getElementById('rwa-st-info').onclick = () => {
|
|
1298
1319
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1299
1320
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1321
|
+
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1300
1322
|
const panel = document.getElementById('rwa-info-panel');
|
|
1301
1323
|
if (!panel.classList.contains('open')) panel.innerHTML = renderInfoPanel();
|
|
1302
1324
|
panel.classList.toggle('open');
|
|
@@ -1307,6 +1329,17 @@ function buildUI() {
|
|
|
1307
1329
|
if (panel && panel.classList.contains('open')) { panel.classList.remove('open'); return; }
|
|
1308
1330
|
openSkinPanel();
|
|
1309
1331
|
};
|
|
1332
|
+
// ↗ connected share — open re-renders from the live record so the panel
|
|
1333
|
+
// always reflects the current connection + freshness. Mutually exclusive
|
|
1334
|
+
// with the other three panels.
|
|
1335
|
+
document.getElementById('rwa-st-share').onclick = () => {
|
|
1336
|
+
const panel = document.getElementById('rwa-share-panel');
|
|
1337
|
+
if (panel.classList.contains('open')) { panel.classList.remove('open'); return; }
|
|
1338
|
+
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1339
|
+
document.getElementById('rwa-info-panel').classList.remove('open');
|
|
1340
|
+
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1341
|
+
renderSharePanel().then(() => panel.classList.add('open'));
|
|
1342
|
+
};
|
|
1310
1343
|
document.getElementById('rwa-st-commit').onclick = commit;
|
|
1311
1344
|
|
|
1312
1345
|
const pal = document.getElementById('rwa-pal'), inp = document.getElementById('rwa-pal-inp'), go = document.getElementById('rwa-pal-go');
|
|
@@ -4159,6 +4192,7 @@ HARD RULES: colors are hex strings only (e.g. "#c0392b"); fonts are ONE of the f
|
|
|
4159
4192
|
if (!panel) return;
|
|
4160
4193
|
const setP = document.getElementById('rwa-set-panel'); if (setP) setP.classList.remove('open');
|
|
4161
4194
|
const infoP = document.getElementById('rwa-info-panel'); if (infoP) infoP.classList.remove('open');
|
|
4195
|
+
const shareP = document.getElementById('rwa-share-panel'); if (shareP) shareP.classList.remove('open');
|
|
4162
4196
|
const active = currentSkinName();
|
|
4163
4197
|
panel.innerHTML =
|
|
4164
4198
|
'<div class="rwa-skin-hd"><span>Skins</span><span>' + (active || '—') + '</span></div>'
|
|
@@ -6154,6 +6188,155 @@ function renderInfoPanel() {
|
|
|
6154
6188
|
].join('');
|
|
6155
6189
|
}
|
|
6156
6190
|
|
|
6191
|
+
// ─── Connected share (the ↗ panel) ──────────────────────────────────
|
|
6192
|
+
// A rewritable can be CONNECTED to a stable URL: "Create share link" POSTs the
|
|
6193
|
+
// full current file bytes (buildFile output — exactly the ⌘S artifact) to the
|
|
6194
|
+
// share service; "Publish this version" re-POSTs to the same short under the
|
|
6195
|
+
// returned update token. The record {short,url,token,publishedHash,publishedAt}
|
|
6196
|
+
// lives in rwa_state — machine-local on purpose: the token is a capability,
|
|
6197
|
+
// and buildFile only serializes INLINE_DOC, so neither can ever travel in the
|
|
6198
|
+
// file. The copy says VERSION on purpose: the link shows the last published
|
|
6199
|
+
// version, not live edits (docs/plans/2026-06-11-save-affordance-framings.md
|
|
6200
|
+
// §7c). These are the ONLY fetches the runtime makes outside the agent
|
|
6201
|
+
// backends, each behind an explicit user gesture — offline-first holds.
|
|
6202
|
+
const shareBaseUrl = () =>
|
|
6203
|
+
(sessionStorage.getItem(RWA.K_SHARE_BASE) || '').trim() || RWA.SHARE_BASE;
|
|
6204
|
+
|
|
6205
|
+
// Hash + file bytes from ONE doc read, so publishedHash always describes the
|
|
6206
|
+
// exact bytes that went out (no mid-flight-edit skew).
|
|
6207
|
+
async function shareSnapshot() {
|
|
6208
|
+
const d = await getDoc();
|
|
6209
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(d));
|
|
6210
|
+
const hash = [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
6211
|
+
return { text: buildFile(d), hash };
|
|
6212
|
+
}
|
|
6213
|
+
async function shareDocHashHex() { return (await shareSnapshot()).hash; }
|
|
6214
|
+
|
|
6215
|
+
const getShareConn = () => idbGet(RWA.STATE, 'share_conn');
|
|
6216
|
+
const setShareConn = v => idbPut(RWA.STATE, v, 'share_conn');
|
|
6217
|
+
const clearShareConn = () => idbDel(RWA.STATE, 'share_conn');
|
|
6218
|
+
|
|
6219
|
+
class ShareError extends Error { constructor(code, msg) { super(msg || code); this.code = code; } }
|
|
6220
|
+
|
|
6221
|
+
async function shareFetch(path, { method = 'POST', token, body } = {}) {
|
|
6222
|
+
const headers = {};
|
|
6223
|
+
if (body != null) headers['Content-Type'] = 'text/html';
|
|
6224
|
+
if (token) headers.Authorization = 'Bearer ' + token;
|
|
6225
|
+
try {
|
|
6226
|
+
return await fetch(shareBaseUrl() + path, { method, headers, body });
|
|
6227
|
+
} catch (_) {
|
|
6228
|
+
throw new ShareError('share_unreachable', 'Sharing service unreachable — check your connection and try again.');
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
|
|
6232
|
+
async function shareErrDetail(res) {
|
|
6233
|
+
try { return (await res.json()).error || ('http ' + res.status); } catch (_) { return 'http ' + res.status; }
|
|
6234
|
+
}
|
|
6235
|
+
|
|
6236
|
+
async function shareCreate() {
|
|
6237
|
+
const snap = await shareSnapshot();
|
|
6238
|
+
const res = await shareFetch('/share', { body: snap.text });
|
|
6239
|
+
if (res.status !== 201) throw new ShareError('share_failed', 'Could not create the share link (' + await shareErrDetail(res) + ').');
|
|
6240
|
+
const out = await res.json();
|
|
6241
|
+
const conn = { short: out.short, url: out.url, token: out.token, publishedHash: snap.hash, publishedAt: Date.now() };
|
|
6242
|
+
await setShareConn(conn);
|
|
6243
|
+
return conn;
|
|
6244
|
+
}
|
|
6245
|
+
|
|
6246
|
+
async function shareUpdate() {
|
|
6247
|
+
const conn = await getShareConn();
|
|
6248
|
+
if (!conn) throw new ShareError('share_not_connected', 'Not connected to a link yet.');
|
|
6249
|
+
const snap = await shareSnapshot();
|
|
6250
|
+
const res = await shareFetch('/share/' + conn.short, { token: conn.token, body: snap.text });
|
|
6251
|
+
if (res.status === 401 || res.status === 404 || res.status === 410) {
|
|
6252
|
+
// The capability is dead (unshared elsewhere, expired, or revoked). Keep
|
|
6253
|
+
// nothing stale around — the honest state is "not connected".
|
|
6254
|
+
await clearShareConn();
|
|
6255
|
+
throw new ShareError('share_connection_lost', 'This link can no longer be updated — create a new one.');
|
|
6256
|
+
}
|
|
6257
|
+
if (res.status !== 200) throw new ShareError('share_failed', 'Could not publish this version (' + await shareErrDetail(res) + ').');
|
|
6258
|
+
const next = { ...conn, publishedHash: snap.hash, publishedAt: Date.now() };
|
|
6259
|
+
await setShareConn(next);
|
|
6260
|
+
return next;
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
async function shareUnshare() {
|
|
6264
|
+
const conn = await getShareConn();
|
|
6265
|
+
if (!conn) return;
|
|
6266
|
+
// A network failure throws BEFORE the clear — an unreachable service must
|
|
6267
|
+
// not silently orphan a live public link. 401/404 mean the share is already
|
|
6268
|
+
// gone server-side; clearing is the honest state either way.
|
|
6269
|
+
await shareFetch('/share/' + conn.short, { method: 'DELETE', token: conn.token });
|
|
6270
|
+
await clearShareConn();
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
function shareRelTime(ts) {
|
|
6274
|
+
const m = Math.round((Date.now() - ts) / 60000);
|
|
6275
|
+
if (m < 1) return 'just now';
|
|
6276
|
+
if (m < 60) return m + ' min ago';
|
|
6277
|
+
const h = Math.round(m / 60);
|
|
6278
|
+
if (h < 24) return h + ' h ago';
|
|
6279
|
+
return Math.round(h / 24) + ' d ago';
|
|
6280
|
+
}
|
|
6281
|
+
|
|
6282
|
+
// One in-flight share action at a time; the panel re-renders disabled while
|
|
6283
|
+
// busy and re-enabled (with an error message, if any) after.
|
|
6284
|
+
let shareBusy = false;
|
|
6285
|
+
async function shareAction(fn) {
|
|
6286
|
+
if (shareBusy) return;
|
|
6287
|
+
shareBusy = true;
|
|
6288
|
+
try { await renderSharePanel(); } catch (_) {}
|
|
6289
|
+
let msg = null;
|
|
6290
|
+
try { await fn(); }
|
|
6291
|
+
catch (e) { msg = (e && e.message) || String(e); }
|
|
6292
|
+
shareBusy = false;
|
|
6293
|
+
await renderSharePanel(msg);
|
|
6294
|
+
}
|
|
6295
|
+
|
|
6296
|
+
function copyShareUrl(url) {
|
|
6297
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
6298
|
+
navigator.clipboard.writeText(url)
|
|
6299
|
+
.then(() => renderSharePanel('Link copied.'))
|
|
6300
|
+
.catch(() => {});
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
async function renderSharePanel(msg) {
|
|
6305
|
+
const panel = document.getElementById('rwa-share-panel');
|
|
6306
|
+
if (!panel) return;
|
|
6307
|
+
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
6308
|
+
const note = msg ? '<div class="rwa-share-msg">' + esc(msg) + '</div>' : '';
|
|
6309
|
+
const dis = shareBusy ? ' disabled' : '';
|
|
6310
|
+
const conn = await getShareConn();
|
|
6311
|
+
if (!conn) {
|
|
6312
|
+
panel.innerHTML = [
|
|
6313
|
+
'<h4>Share at a link</h4>',
|
|
6314
|
+
'<p>Anyone with the link sees the version you publish — not your live edits.</p>',
|
|
6315
|
+
note,
|
|
6316
|
+
'<div class="rwa-share-actions"><button type="button" id="rwa-share-create"' + dis + '>Create share link</button></div>',
|
|
6317
|
+
].join('');
|
|
6318
|
+
document.getElementById('rwa-share-create').onclick = () => shareAction(shareCreate);
|
|
6319
|
+
return;
|
|
6320
|
+
}
|
|
6321
|
+
const current = (await shareDocHashHex()) === conn.publishedHash;
|
|
6322
|
+
panel.innerHTML = [
|
|
6323
|
+
'<h4>Shared at a link</h4>',
|
|
6324
|
+
'<div class="rwa-share-url" id="rwa-share-url" title="click to copy">' + esc(conn.url) + '</div>',
|
|
6325
|
+
'<div class="rwa-share-fresh" id="rwa-share-fresh">Published ' + esc(shareRelTime(conn.publishedAt)) + ' · ' +
|
|
6326
|
+
(current ? 'the link shows this version' : 'behind your latest edits') + '</div>',
|
|
6327
|
+
note,
|
|
6328
|
+
'<div class="rwa-share-actions">',
|
|
6329
|
+
'<button type="button" id="rwa-share-update"' + dis + '>Publish this version</button>',
|
|
6330
|
+
'<button type="button" id="rwa-share-copy">Copy link</button>',
|
|
6331
|
+
'<button type="button" id="rwa-share-stop"' + dis + '>Stop sharing</button>',
|
|
6332
|
+
'</div>',
|
|
6333
|
+
].join('');
|
|
6334
|
+
document.getElementById('rwa-share-update').onclick = () => shareAction(shareUpdate);
|
|
6335
|
+
document.getElementById('rwa-share-stop').onclick = () => shareAction(shareUnshare);
|
|
6336
|
+
document.getElementById('rwa-share-copy').onclick = () => copyShareUrl(conn.url);
|
|
6337
|
+
document.getElementById('rwa-share-url').onclick = () => copyShareUrl(conn.url);
|
|
6338
|
+
}
|
|
6339
|
+
|
|
6157
6340
|
// ─── First-party 'presentation' view provider (spec §5.10) ──────────
|
|
6158
6341
|
// WRAP-IN-PLACE: split the doc at <h1>/<h2> boundaries into
|
|
6159
6342
|
// <section class="rwa-slide"> wrappers WITHOUT reordering. SECTION ∉
|