wiki-formant 0.105.0 → 0.105.1

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.
@@ -31,7 +31,7 @@
31
31
  // the item back in, so a run's whole-state write cannot drop it. Every mode
32
32
  // reads and writes under the row lock. Only `state` in the page's metadata is
33
33
  // written; other keys are kept. A revision bumps the page's version a minor.
34
- import { PINNED, addBacklog, compact, lateBacklog, render, staleWrite } from '../dist/maintenance.js';
34
+ import { PINNED, addBacklog, compact, isOpen, lateBacklog, render, staleWrite } from '../dist/maintenance.js';
35
35
  import { bump } from '../dist/versioning.js';
36
36
  import { arg, connect, invalidate } from './_cli.mjs';
37
37
 
@@ -131,7 +131,7 @@ try {
131
131
  const kept = new Set(state.backlog.map(b => b.item));
132
132
  for (const b of input.backlog ?? []) {
133
133
  const item = typeof b === 'string' ? b : b.item;
134
- if (PINNED.test(item ?? '') && (typeof b === 'string' || (b.status ?? 'open') === 'open') && !kept.has(item)) {
134
+ if (PINNED.test(item ?? '') && isOpen(b) && !kept.has(item)) {
135
135
  console.log(` flag dropped over the cap: ${item}`);
136
136
  }
137
137
  }
@@ -32,9 +32,11 @@ const line = (html) => ({ dangerouslySetInnerHTML: { __html: html } });
32
32
  * whatever the consumer's highlighter attached to it on every tab press.
33
33
  */
34
34
  export function CodeTabsView({ tabs, highlighted = false }) {
35
- const [activeTab, setActiveTab] = useState(0);
35
+ const [picked, setActiveTab] = useState(0);
36
36
  if (!tabs.length)
37
37
  return null;
38
+ // Clamped, so tabs removed under the selection leave the last one showing.
39
+ const activeTab = Math.min(picked, tabs.length - 1);
38
40
  return (_jsxs("div", { className: "code-tabs", children: [_jsx("div", { className: "code-tabs-list", children: tabs.map((tab, i) => (_jsx("button", { type: "button", className: cx('code-tabs-btn', i === activeTab && 'code-tabs-btn-active'), onClick: () => setActiveTab(i), children: tab.label }, i))) }), tabs.map((tab, i) => highlighted ? (_jsx("div", { className: "code-tabs-panel", hidden: i !== activeTab, dangerouslySetInnerHTML: { __html: tab.code } }, i)) : (_jsx("div", { className: "code-tabs-panel", hidden: i !== activeTab, children: _jsx("pre", { children: _jsx("code", { className: tab.language ? `language-${tab.language}` : undefined, children: tab.code }) }) }, i)))] }));
39
41
  }
40
42
  /**
package/dist/dom.js CHANGED
@@ -35,13 +35,15 @@ export function addCopyButtons(root, options = {}) {
35
35
  btn.className = className;
36
36
  btn.setAttribute('aria-label', label);
37
37
  btn.innerHTML = COPY_SVG;
38
+ let revert;
38
39
  btn.onclick = () => {
39
40
  const code = pre.querySelector('code')?.textContent || pre.textContent || '';
40
41
  // A rejected clipboard write (permissions, insecure origin) must not
41
42
  // leave an unhandled rejection behind — the button simply does nothing.
42
43
  navigator.clipboard.writeText(code).then(() => {
43
44
  btn.innerHTML = CHECK_SVG;
44
- setTimeout(() => {
45
+ clearTimeout(revert);
46
+ revert = setTimeout(() => {
45
47
  btn.innerHTML = COPY_SVG;
46
48
  }, revertAfter);
47
49
  }, () => { });
package/dist/editor.js CHANGED
@@ -482,13 +482,17 @@ export function useWikiEditor({ value, onChange, placeholder = '', nodes = [], p
482
482
  if (!file || !editor)
483
483
  return;
484
484
  setIsUploading(true);
485
- const url = await uploadImage(file);
486
- if (url)
487
- editor.chain().focus().setImage({ src: url }).run();
488
- setIsUploading(false);
489
- // Cleared so choosing the same file twice in a row still fires a change.
490
- if (fileInputRef.current)
491
- fileInputRef.current.value = '';
485
+ try {
486
+ const url = await uploadImage(file);
487
+ if (url)
488
+ editor.chain().focus().setImage({ src: url }).run();
489
+ }
490
+ finally {
491
+ setIsUploading(false);
492
+ // Cleared so choosing the same file twice in a row still fires a change.
493
+ if (fileInputRef.current)
494
+ fileInputRef.current.value = '';
495
+ }
492
496
  }, [editor, uploadImage]);
493
497
  const triggerUpload = useCallback(() => fileInputRef.current?.click(), []);
494
498
  return { editor, fileInputRef, isUploading, handleFileChange, triggerUpload };
@@ -11,7 +11,7 @@ interface SourceEntry {
11
11
  export type BacklogEntry = string | {
12
12
  date?: string;
13
13
  item: string;
14
- status?: string;
14
+ status?: string | null;
15
15
  };
16
16
  type FeedbackEntry = string | {
17
17
  date?: string;
@@ -31,13 +31,19 @@ const JOURNAL = /^run\s+\d+\b/i;
31
31
  export const PINNED = /FLAG FOR A HUMAN/i;
32
32
  const BANKED = /\bbanked\b|bot[- ]?(wall|block)|\b(403|429|999)\b|cloudflare/i;
33
33
  const itemOf = (b) => (typeof b === 'string' ? b : b.item);
34
+ /**
35
+ * @internal The one test of open, which the bin shares: a bare string, or no
36
+ * status, is open; `compact` deletes anything else for good.
37
+ */
38
+ export const isOpen = (b) => typeof b === 'string' || (b.status ?? 'open') === 'open';
34
39
  const recency = (a, b) => String(b[1]?.lastChecked ?? '').localeCompare(String(a[1]?.lastChecked ?? ''));
35
40
  /** Drops what is closed or a diary entry, and caps what is append-only. Newest first. */
36
41
  export function compact(state) {
37
42
  // Undated items fall back to append order: a later index is newer.
38
43
  const open = (state.backlog ?? [])
39
- .map((b, i) => ({ status: 'open', ...(typeof b === 'string' ? { date: '', item: b } : b), i }))
40
- .filter(b => b.status === 'open' && b.item && (PINNED.test(b.item) || !JOURNAL.test(b.item)))
44
+ .filter(isOpen)
45
+ .map((b, i) => ({ ...(typeof b === 'string' ? { date: '', item: b } : b), status: 'open', i }))
46
+ .filter(b => b.item && (PINNED.test(b.item) || !JOURNAL.test(b.item)))
41
47
  .sort((a, b) => String(b.date ?? '').localeCompare(String(a.date ?? '')) || b.i - a.i);
42
48
  const backlog = [...open.filter(b => PINNED.test(b.item)).slice(0, MAX.flags), ...open.filter(b => !PINNED.test(b.item))]
43
49
  .slice(0, MAX.backlog)
package/dist/react.js CHANGED
@@ -394,6 +394,10 @@ export function useTypeahead({ fetch, onPick, onEscape, delay = 200, minLength =
394
394
  setHighlight(h => (h - 1 + items.length) % items.length);
395
395
  }
396
396
  else if (e.key === 'Enter') {
397
+ // The list answers the last settled query. Typed past it, Enter falls
398
+ // through to the form, so it submits what was typed, not a stale pick.
399
+ if (query !== debounced || isSearching)
400
+ return;
397
401
  e.preventDefault();
398
402
  const target = items[highlight];
399
403
  if (target)
@@ -463,7 +467,10 @@ export function useLinkPreview({ eligible, fetch, cardClassName = 'link-preview-
463
467
  clearTimeout(hideTimer.current);
464
468
  return;
465
469
  }
470
+ // A new link retires the last one's card at once: the grace is for
471
+ // crossing into that card, and the cursor has gone somewhere else.
466
472
  clearTimers();
473
+ setCard(null);
467
474
  activeHref.current = href;
468
475
  showTimer.current = setTimeout(async () => {
469
476
  let data = cache.current.get(href);
@@ -754,11 +761,17 @@ export function restoreViaPost(endpoint) {
754
761
  export function useRevisionRestore(restore, options) {
755
762
  const [busyId, setBusyId] = useState(null);
756
763
  const [error, setError] = useState(null);
764
+ // One restore at a time: a second, started while the first is in flight,
765
+ // would have its "Restoring…" cleared by the first one finishing.
766
+ const inFlight = useRef(false);
757
767
  const { onRestored, confirm = (label) => `Restore ${label}? The current state stays in the history.` } = options;
758
768
  const run = useCallback(async (id, label) => {
769
+ if (inFlight.current)
770
+ return;
759
771
  const question = confirm(label);
760
772
  if (question && !window.confirm(question))
761
773
  return;
774
+ inFlight.current = true;
762
775
  setBusyId(id);
763
776
  setError(null);
764
777
  try {
@@ -769,6 +782,7 @@ export function useRevisionRestore(restore, options) {
769
782
  onRestored();
770
783
  }
771
784
  finally {
785
+ inFlight.current = false;
772
786
  setBusyId(null);
773
787
  }
774
788
  }, [restore, onRestored, confirm]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.105.0",
3
+ "version": "0.105.1",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",