@wildwinter/app-shell 0.49.0 → 0.51.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.
Files changed (46) hide show
  1. package/README.md +37 -0
  2. package/dist/{chunk-FGAPO4N5.js → chunk-B56FWJ5Y.js} +21 -12
  3. package/dist/chunk-B56FWJ5Y.js.map +1 -0
  4. package/dist/chunk-ELEDQRZ7.js +435 -0
  5. package/dist/chunk-ELEDQRZ7.js.map +1 -0
  6. package/dist/controls.css +107 -0
  7. package/dist/controls.css.map +1 -1
  8. package/dist/history.cjs +194 -0
  9. package/dist/history.cjs.map +1 -0
  10. package/dist/history.d.cts +111 -0
  11. package/dist/history.d.ts +111 -0
  12. package/dist/history.js +188 -0
  13. package/dist/history.js.map +1 -0
  14. package/dist/index.cjs +368 -9
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +2 -2
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +2 -2
  19. package/dist/{property-list-BPGU3rhF.d.cts → property-list-DlpxIkv6.d.cts} +9 -0
  20. package/dist/{property-list-BPGU3rhF.d.ts → property-list-DlpxIkv6.d.ts} +9 -0
  21. package/dist/property-list.cjs +18 -9
  22. package/dist/property-list.cjs.map +1 -1
  23. package/dist/property-list.d.cts +1 -1
  24. package/dist/property-list.d.ts +1 -1
  25. package/dist/property-list.js +2 -2
  26. package/dist/value-control.cjs +584 -0
  27. package/dist/value-control.cjs.map +1 -1
  28. package/dist/value-control.d.cts +61 -1
  29. package/dist/value-control.d.ts +61 -1
  30. package/dist/value-control.js +1 -1
  31. package/dist/value-rows.cjs +1104 -0
  32. package/dist/value-rows.cjs.map +1 -0
  33. package/dist/value-rows.d.cts +71 -0
  34. package/dist/value-rows.d.ts +71 -0
  35. package/dist/value-rows.js +298 -0
  36. package/dist/value-rows.js.map +1 -0
  37. package/dist/window-guard.cjs +74 -0
  38. package/dist/window-guard.cjs.map +1 -0
  39. package/dist/window-guard.d.cts +32 -0
  40. package/dist/window-guard.d.ts +32 -0
  41. package/dist/window-guard.js +67 -0
  42. package/dist/window-guard.js.map +1 -0
  43. package/package.json +26 -4
  44. package/dist/chunk-EAE7UWQ5.js +0 -84
  45. package/dist/chunk-EAE7UWQ5.js.map +0 -1
  46. package/dist/chunk-FGAPO4N5.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/controls.css"],"sourcesContent":["/* ============================================================================\n The family's control grammar: one button, one field, one segmented control,\n one pill, one empty-state line, one keycap. Class-scoped throughout, never\n an element reset: a stylesheet that restyles every <button> in a window is a\n blast radius, and the two apps had five spellings of \"primary button\" using\n four foreground tokens between them (ui-review-2026-09, finding 20).\n\n Reads the token grammar (tokens.css) and the app's palette (--surface,\n --card, --ink, --muted, --line, --accent, --danger, --on-accent; optional\n --accent-soft, --line-soft, --font-mono). The keyboard focus ring is the\n family's one rule in tokens.css, so nothing here draws another.\n\n \"Controls have edges\" (design-language.md, 2026-09-16): a button has a\n border or a fill at rest, and text a person can read without hovering.\n Ghost styling is ONLY for icon-only toolbar buttons in a bar that is itself\n the affordance. And \"Motion is interpolation\", as amended: the only thing\n that transitions on hover is colour, at --dur-quick. No lift, no shadow\n change, no scale.\n ============================================================================ */\n\n/* --- buttons ---------------------------------------------------------------- */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 0.35em;\n padding: 5px 11px;\n border: 1px solid var(--line); border-radius: var(--radius-sm);\n background: var(--surface); color: var(--ink);\n font: 600 12.5px var(--font-ui); letter-spacing: 0.02em; line-height: 1.2;\n cursor: pointer; white-space: nowrap;\n -webkit-user-select: none; user-select: none;\n transition: color var(--dur-quick) var(--ease-standard),\n background var(--dur-quick) var(--ease-standard),\n border-color var(--dur-quick) var(--ease-standard);\n}\n.btn:hover:not(:disabled) {\n border-color: var(--accent);\n background: var(--accent-soft, color-mix(in oklab, var(--accent) 10%, transparent));\n}\n.btn:disabled { opacity: 0.45; cursor: default; }\n\n/* The one action a dialog or a bar is for: accent fill, the palette's own ink\n on it (never a raw white, which read white-on-tan on the dark palettes). */\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }\n.btn.primary:hover:not(:disabled) {\n background: color-mix(in oklab, var(--accent) 88%, var(--ink));\n border-color: color-mix(in oklab, var(--accent) 88%, var(--ink));\n color: var(--on-accent);\n}\n\n/* The destructive action, and it names its object (\"Delete scene\"). */\n.btn.danger { background: var(--danger); border-color: var(--danger); color: var(--surface); }\n.btn.danger:hover:not(:disabled) {\n background: color-mix(in oklab, var(--danger) 88%, var(--ink));\n border-color: color-mix(in oklab, var(--danger) 88%, var(--ink));\n color: var(--surface);\n}\n\n/* ONLY for an icon-only button in a toolbar: borderless, muted, a wash on\n hover. A text action drawn this way is the \"bare muted text on the ground\"\n the design language rules out. */\n.btn.ghost { border-color: transparent; background: none; color: var(--muted); }\n.btn.ghost:hover:not(:disabled) {\n border-color: transparent; color: var(--ink);\n background: var(--accent-soft, color-mix(in oklab, var(--accent) 10%, transparent));\n}\n\n/* A square button holding one glyph, centred explicitly: a bare <button>\n centres by the font's metrics plus UA padding, which parks an arrow up-left\n of the square's visual centre. */\n.btn.icon { width: 28px; height: 28px; padding: 0; line-height: 1; font-size: 13.5px; }\n\n/* --- fields ----------------------------------------------------------------- */\n.field {\n font: inherit; font-size: 13.5px; line-height: 1.4;\n padding: 6px 9px;\n border: 1px solid var(--line); border-radius: var(--radius-sm);\n background: var(--card); color: var(--ink);\n -webkit-user-select: text; user-select: text;\n transition: border-color var(--dur-quick) var(--ease-standard);\n}\n.field:focus-visible { border-color: var(--accent); outline-offset: 0; }\n/* A placeholder is a hint, not content: lighter and italic, its opacity\n pinned so no engine fades an already-muted colour a second time. */\n.field::placeholder { color: var(--muted); font-style: italic; opacity: 1; }\n/* `.invalid` is the dup guard's (a value that clashes with another row);\n `.illegal` is a value that breaks a rule. Both can be true at once, so they\n are separate classes that draw the same. */\n.field.invalid, .field.illegal { border-color: var(--danger); }\n.field:disabled { opacity: 0.55; cursor: default; }\ntextarea.field { resize: vertical; }\n\n/* --- segmented control ------------------------------------------------------ */\n/* One bordered strip, options divided by hairlines, the chosen one washed. Seven\n hand-rolled copies across the two apps agreed on everything but the radius\n (finding 21); this is the one they converge on. */\n.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }\n.seg-opt {\n background: none; border: 0; cursor: pointer;\n color: var(--muted); font: 600 12px var(--font-ui); padding: 3px 9.5px; white-space: nowrap;\n transition: color var(--dur-quick) var(--ease-standard), background var(--dur-quick) var(--ease-standard);\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--line); }\n.seg-opt:hover:not(.on) { color: var(--ink); }\n.seg-opt.on {\n color: var(--accent); cursor: default;\n background: var(--accent-soft, color-mix(in oklab, var(--accent) 14%, transparent));\n}\n\n/* A quality's value as its ladder (value-control.ts rungStrip): the rungs in order, the current one marked.\n Stage names keep the mono voice values use. An unset value's fallback rung is marked quietly; a value that\n is no longer a stage is shown after the ladder, in the warning colour, until a rung is picked. */\n.shell-rungs { flex-wrap: wrap; }\n.shell-rung { font-family: var(--font-mono); font-weight: 400; padding: 2px 7px; }\n.shell-rung.on { font-weight: 600; }\n.shell-rung.fallback { color: var(--ink); text-decoration: underline dotted; text-underline-offset: 3px; }\n.shell-rung-default { font-family: var(--font-ui); }\n.shell-rung.stray { color: var(--warn, var(--danger, var(--accent))); text-decoration: line-through; cursor: default; }\nspan.shell-rung { cursor: default; }\n\n/* --- small things ----------------------------------------------------------- */\n/* A pill: a small bordered label (a class, a count, a state). */\n.pill {\n display: inline-flex; align-items: center; gap: 4px;\n font: 11.5px/1.3 var(--font-ui); color: var(--muted);\n border: 1px solid var(--line-soft, var(--line)); border-radius: var(--radius-pill);\n padding: 1px 7px; white-space: nowrap;\n}\n\n/* The inline empty state: one muted italic sentence where a list would be. */\n.empty { margin: 0; color: var(--muted); font: italic 13px/1.45 var(--font-ui); }\n\n/* A keycap. A key hint is an element, never a typed \"↑↓ move\" string. The\n element rule is the floor for a hand-written <kbd>; the shell's own hint\n helpers (keys.ts) stamp `.shell-kbd`, drawn in keys.css. */\nkbd {\n display: inline-block;\n font: 11px/1.3 var(--font-mono, ui-monospace, monospace);\n color: var(--muted);\n border: 1px solid var(--line); border-bottom-width: 2px; border-radius: var(--radius-sm);\n padding: 1px 5px; vertical-align: baseline;\n}\n\n/* --- metadata and breadcrumbs ---------------------------------------------- */\n/* A metadata line (`metaLine` in dom.ts): parts in a row, each after the first\n led by a DRAWN 3px disc. Never a \"·\" character: that is a typed separator,\n and it renders at whatever weight and height the symbol font has. The disc\n is the muted ink, the row inherits its size from wherever it sits. */\n.shell-meta { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; min-width: 0; }\n.shell-meta-part { display: inline-flex; align-items: center; gap: 8px; min-width: 0; white-space: nowrap; }\n.shell-meta-part + .shell-meta-part::before {\n content: \"\"; flex: none; width: 3px; height: 3px;\n border-radius: var(--radius-pill); background: var(--muted);\n}\n\n/* A breadcrumb trail (`breadcrumb` in dom.ts): crumbs with the vocabulary's\n `forward` chevron drawn between them. A crumb that goes somewhere is a\n button coloured as a link (it reads as a control with the pointer parked);\n the last crumb is where the reader is, in the ink. */\n.shell-crumbs { display: inline-flex; align-items: center; gap: 2px; min-width: 0; font: 12.5px/1.3 var(--font-ui); }\n.shell-crumb { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }\n.shell-crumb[aria-current] { color: var(--ink); }\n.shell-crumb-sep { flex: none; color: var(--muted); opacity: 0.7; }\n.shell-crumb-link {\n font: inherit; background: none; border: 0; padding: 0 2px; margin: 0;\n border-radius: var(--radius-sm); color: var(--accent); cursor: pointer;\n transition: color var(--dur-quick) var(--ease-standard);\n}\n.shell-crumb-link:hover { color: var(--ink); text-decoration: underline; }\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/controls.css"],"sourcesContent":["/* ============================================================================\n The family's control grammar: one button, one field, one segmented control,\n one pill, one empty-state line, one keycap. Class-scoped throughout, never\n an element reset: a stylesheet that restyles every <button> in a window is a\n blast radius, and the two apps had five spellings of \"primary button\" using\n four foreground tokens between them (ui-review-2026-09, finding 20).\n\n Reads the token grammar (tokens.css) and the app's palette (--surface,\n --card, --ink, --muted, --line, --accent, --danger, --on-accent; optional\n --accent-soft, --line-soft, --font-mono). The keyboard focus ring is the\n family's one rule in tokens.css, so nothing here draws another.\n\n \"Controls have edges\" (design-language.md, 2026-09-16): a button has a\n border or a fill at rest, and text a person can read without hovering.\n Ghost styling is ONLY for icon-only toolbar buttons in a bar that is itself\n the affordance. And \"Motion is interpolation\", as amended: the only thing\n that transitions on hover is colour, at --dur-quick. No lift, no shadow\n change, no scale.\n ============================================================================ */\n\n/* --- buttons ---------------------------------------------------------------- */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 0.35em;\n padding: 5px 11px;\n border: 1px solid var(--line); border-radius: var(--radius-sm);\n background: var(--surface); color: var(--ink);\n font: 600 12.5px var(--font-ui); letter-spacing: 0.02em; line-height: 1.2;\n cursor: pointer; white-space: nowrap;\n -webkit-user-select: none; user-select: none;\n transition: color var(--dur-quick) var(--ease-standard),\n background var(--dur-quick) var(--ease-standard),\n border-color var(--dur-quick) var(--ease-standard);\n}\n.btn:hover:not(:disabled) {\n border-color: var(--accent);\n background: var(--accent-soft, color-mix(in oklab, var(--accent) 10%, transparent));\n}\n.btn:disabled { opacity: 0.45; cursor: default; }\n\n/* The one action a dialog or a bar is for: accent fill, the palette's own ink\n on it (never a raw white, which read white-on-tan on the dark palettes). */\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }\n.btn.primary:hover:not(:disabled) {\n background: color-mix(in oklab, var(--accent) 88%, var(--ink));\n border-color: color-mix(in oklab, var(--accent) 88%, var(--ink));\n color: var(--on-accent);\n}\n\n/* The destructive action, and it names its object (\"Delete scene\"). */\n.btn.danger { background: var(--danger); border-color: var(--danger); color: var(--surface); }\n.btn.danger:hover:not(:disabled) {\n background: color-mix(in oklab, var(--danger) 88%, var(--ink));\n border-color: color-mix(in oklab, var(--danger) 88%, var(--ink));\n color: var(--surface);\n}\n\n/* ONLY for an icon-only button in a toolbar: borderless, muted, a wash on\n hover. A text action drawn this way is the \"bare muted text on the ground\"\n the design language rules out. */\n.btn.ghost { border-color: transparent; background: none; color: var(--muted); }\n.btn.ghost:hover:not(:disabled) {\n border-color: transparent; color: var(--ink);\n background: var(--accent-soft, color-mix(in oklab, var(--accent) 10%, transparent));\n}\n\n/* A square button holding one glyph, centred explicitly: a bare <button>\n centres by the font's metrics plus UA padding, which parks an arrow up-left\n of the square's visual centre. */\n.btn.icon { width: 28px; height: 28px; padding: 0; line-height: 1; font-size: 13.5px; }\n\n/* --- fields ----------------------------------------------------------------- */\n.field {\n font: inherit; font-size: 13.5px; line-height: 1.4;\n padding: 6px 9px;\n border: 1px solid var(--line); border-radius: var(--radius-sm);\n background: var(--card); color: var(--ink);\n -webkit-user-select: text; user-select: text;\n transition: border-color var(--dur-quick) var(--ease-standard);\n}\n.field:focus-visible { border-color: var(--accent); outline-offset: 0; }\n/* A placeholder is a hint, not content: lighter and italic, its opacity\n pinned so no engine fades an already-muted colour a second time. */\n.field::placeholder { color: var(--muted); font-style: italic; opacity: 1; }\n/* `.invalid` is the dup guard's (a value that clashes with another row);\n `.illegal` is a value that breaks a rule. Both can be true at once, so they\n are separate classes that draw the same. */\n.field.invalid, .field.illegal { border-color: var(--danger); }\n.field:disabled { opacity: 0.55; cursor: default; }\ntextarea.field { resize: vertical; }\n\n/* --- segmented control ------------------------------------------------------ */\n/* One bordered strip, options divided by hairlines, the chosen one washed. Seven\n hand-rolled copies across the two apps agreed on everything but the radius\n (finding 21); this is the one they converge on. */\n.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }\n.seg-opt {\n background: none; border: 0; cursor: pointer;\n color: var(--muted); font: 600 12px var(--font-ui); padding: 3px 9.5px; white-space: nowrap;\n transition: color var(--dur-quick) var(--ease-standard), background var(--dur-quick) var(--ease-standard);\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--line); }\n.seg-opt:hover:not(.on) { color: var(--ink); }\n.seg-opt.on {\n color: var(--accent); cursor: default;\n background: var(--accent-soft, color-mix(in oklab, var(--accent) 14%, transparent));\n}\n\n/* A quality's value as its ladder (value-control.ts rungStrip): the rungs in order, the current one marked.\n Stage names keep the mono voice values use. An unset value's fallback rung is marked quietly; a value that\n is no longer a stage is shown after the ladder, in the warning colour, until a rung is picked. */\n.shell-rungs { flex-wrap: wrap; }\n.shell-rung { font-family: var(--font-mono); font-weight: 400; padding: 2px 7px; }\n.shell-rung.on { font-weight: 600; }\n.shell-rung.fallback { color: var(--ink); text-decoration: underline dotted; text-underline-offset: 3px; }\n.shell-rung-default { font-family: var(--font-ui); }\n.shell-rung.stray { color: var(--warn, var(--danger, var(--accent))); text-decoration: line-through; cursor: default; }\nspan.shell-rung { cursor: default; }\n\n/* A declared field's value as a row (value-control.ts fieldRow): the caption, then the control with the\n purpose and any message under it. The grid is the app's (it passes its own row class); these are the\n parts both apps share. A flags value is a picker of the declared values: chips, the set ones washed. */\n.shell-fieldrow-body { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; min-width: 0; }\n.shell-fieldrow-textual .shell-fieldrow-body { align-items: stretch; }\n.shell-fieldrow-text { min-width: 0; }\ntextarea.shell-fieldrow-text { min-height: 3.6em; font-family: inherit; line-height: 1.4; }\n.shell-fieldrow-purpose { margin: 0; font: 12px/1.4 var(--font-ui); color: var(--muted); max-width: 60ch; }\n.shell-fieldrow-problem { margin: 0; font: 12px/1.4 var(--font-ui); color: var(--warn, var(--danger, var(--accent))); }\n.shell-fieldrow.has-problem .shell-fieldrow-text { border-color: var(--warn, var(--danger)); }\n.shell-flags { display: inline-flex; flex-wrap: wrap; gap: 4px; }\n.shell-flag {\n display: inline-flex; align-items: center; border: 1px solid var(--line); border-radius: var(--radius-pill);\n background: none; color: var(--muted); font: 12px var(--font-mono); padding: 1px 8px; cursor: pointer;\n}\n.shell-flag:hover:not(.on):not(span) { color: var(--ink); }\n.shell-flag.on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft, color-mix(in oklab, var(--accent) 14%, transparent)); }\n.shell-flag.fallback { color: var(--ink); border-style: dashed; background: none; }\n.shell-flag-default { font-family: var(--font-ui); }\n.shell-flag-default.on { cursor: default; }\n.shell-flag.stray { color: var(--warn, var(--danger, var(--accent))); border-color: currentColor; background: none; text-decoration: line-through; }\nspan.shell-flag { cursor: default; }\n\n/* --- small things ----------------------------------------------------------- */\n/* A pill: a small bordered label (a class, a count, a state). */\n.pill {\n display: inline-flex; align-items: center; gap: 4px;\n font: 11.5px/1.3 var(--font-ui); color: var(--muted);\n border: 1px solid var(--line-soft, var(--line)); border-radius: var(--radius-pill);\n padding: 1px 7px; white-space: nowrap;\n}\n\n/* The inline empty state: one muted italic sentence where a list would be. */\n.empty { margin: 0; color: var(--muted); font: italic 13px/1.45 var(--font-ui); }\n\n/* A keycap. A key hint is an element, never a typed \"↑↓ move\" string. The\n element rule is the floor for a hand-written <kbd>; the shell's own hint\n helpers (keys.ts) stamp `.shell-kbd`, drawn in keys.css. */\nkbd {\n display: inline-block;\n font: 11px/1.3 var(--font-mono, ui-monospace, monospace);\n color: var(--muted);\n border: 1px solid var(--line); border-bottom-width: 2px; border-radius: var(--radius-sm);\n padding: 1px 5px; vertical-align: baseline;\n}\n\n/* --- metadata and breadcrumbs ---------------------------------------------- */\n/* A metadata line (`metaLine` in dom.ts): parts in a row, each after the first\n led by a DRAWN 3px disc. Never a \"·\" character: that is a typed separator,\n and it renders at whatever weight and height the symbol font has. The disc\n is the muted ink, the row inherits its size from wherever it sits. */\n.shell-meta { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; min-width: 0; }\n.shell-meta-part { display: inline-flex; align-items: center; gap: 8px; min-width: 0; white-space: nowrap; }\n.shell-meta-part + .shell-meta-part::before {\n content: \"\"; flex: none; width: 3px; height: 3px;\n border-radius: var(--radius-pill); background: var(--muted);\n}\n\n/* A breadcrumb trail (`breadcrumb` in dom.ts): crumbs with the vocabulary's\n `forward` chevron drawn between them. A crumb that goes somewhere is a\n button coloured as a link (it reads as a control with the pointer parked);\n the last crumb is where the reader is, in the ink. */\n.shell-crumbs { display: inline-flex; align-items: center; gap: 2px; min-width: 0; font: 12.5px/1.3 var(--font-ui); }\n.shell-crumb { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }\n.shell-crumb[aria-current] { color: var(--ink); }\n.shell-crumb-sep { flex: none; color: var(--muted); opacity: 0.7; }\n.shell-crumb-link {\n font: inherit; background: none; border: 0; padding: 0 2px; margin: 0;\n border-radius: var(--radius-sm); color: var(--accent); cursor: pointer;\n transition: color var(--dur-quick) var(--ease-standard);\n}\n.shell-crumb-link:hover { color: var(--ink); text-decoration: underline; }\n\n/* Structured values (record, map, list): the parts, each a field row of its own, indented under the\n caption, with a map's and a list's own controls beside each part. */\n.shell-valuerow-parts { gap: 6px; padding-left: 10px; border-left: 1px solid var(--line, color-mix(in oklab, var(--ink) 14%, transparent)); }\n.shell-valuerow-part { display: flex; align-items: flex-start; gap: 6px; width: 100%; }\n.shell-valuerow-part > .shell-fieldrow { flex: 1; min-width: 0; }\n.shell-valuerow-controls { display: inline-flex; gap: 2px; padding-top: 2px; }\n.shell-valuerow-controls:empty { display: none; }\n.shell-valuerow-add { display: inline-flex; align-items: center; gap: 4px; }\n.shell-valuerow-key { min-width: 10em; }\n"],"mappings":"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","names":[]}
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var simpleVcLib = require('@wildwinter/simple-vc-lib');
6
+
7
+ // src/history.ts
8
+ function captureBefore(paths) {
9
+ return paths.map((path) => ({ path, content: fs.existsSync(path) ? fs.readFileSync(path, "utf8") : null }));
10
+ }
11
+ async function applyStates(all) {
12
+ const states = all.filter((s) => s.local !== true);
13
+ const writes = states.filter((s) => s.content !== null).map((s) => ({ filePath: s.path, content: s.content }));
14
+ if (writes.length > 0) {
15
+ try {
16
+ const batch = await simpleVcLib.writeTextFilesAsync(writes, "utf8", { allOrNothing: true });
17
+ if (!batch.success) return { ok: false, error: writeFailure(batch.results), wrote: false };
18
+ } catch (e) {
19
+ return { ok: false, error: `couldn't save: ${e instanceof Error ? e.message : String(e)}`, wrote: false };
20
+ }
21
+ }
22
+ const undeleted = [];
23
+ for (const s of states) {
24
+ if (s.content !== null || !fs.existsSync(s.path)) continue;
25
+ try {
26
+ const gone = await simpleVcLib.deleteFileAsync(s.path);
27
+ if (!gone.success) undeleted.push(`${path.basename(s.path)} (${gone.message || gone.status})`);
28
+ } catch (e) {
29
+ undeleted.push(`${path.basename(s.path)} (${e instanceof Error ? e.message : String(e)})`);
30
+ }
31
+ }
32
+ if (undeleted.length > 0) return { ok: false, error: `couldn't delete ${undeleted.join("; ")}`, wrote: writes.length > 0 };
33
+ for (const s of all) {
34
+ if (s.local !== true) continue;
35
+ if (s.content === null) fs.rmSync(s.path, { force: true });
36
+ else fs.writeFileSync(s.path, s.content, "utf8");
37
+ }
38
+ return { ok: true };
39
+ }
40
+ function writeFailure(results) {
41
+ const failed = results.filter((r) => !r.success);
42
+ const refused = failed.filter((r) => r.status === "locked" || r.status === "outOfDate");
43
+ const say = (r) => {
44
+ const name = path.basename(r.filePath);
45
+ const why = r.message || r.status;
46
+ return why.includes(name) ? why : `${name} (${why})`;
47
+ };
48
+ if (refused.length > 0) return `nothing was saved: ${refused.map(say).join("; ")}`;
49
+ return `couldn't save ${failed.map(say).join("; ") || "the project"}`;
50
+ }
51
+ var UNDO_LIMIT = 100;
52
+ function mergeStates(base, override) {
53
+ const byPath = /* @__PURE__ */ new Map();
54
+ for (const s of base) byPath.set(s.path, s);
55
+ for (const s of override) byPath.set(s.path, s);
56
+ return [...byPath.values()];
57
+ }
58
+ var listNames = (paths) => paths.map((p) => path.basename(p)).join(", ");
59
+ var History = class {
60
+ undoStack = [];
61
+ redoStack = [];
62
+ opts;
63
+ constructor(opts = {}) {
64
+ this.opts = opts;
65
+ }
66
+ reset() {
67
+ this.undoStack = [];
68
+ this.redoStack = [];
69
+ }
70
+ get canUndo() {
71
+ return this.undoStack.length > 0;
72
+ }
73
+ get canRedo() {
74
+ return this.redoStack.length > 0;
75
+ }
76
+ /** The labels of the next undo and redo, for a menu ("Undo Rename Card"). */
77
+ get undoLabel() {
78
+ return this.undoStack[this.undoStack.length - 1]?.label;
79
+ }
80
+ get redoLabel() {
81
+ return this.redoStack[this.redoStack.length - 1]?.label;
82
+ }
83
+ /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit
84
+ * continuing, as typing into one field). A new distinct edit clears redo. */
85
+ record(label, key, before, after) {
86
+ const top = this.undoStack[this.undoStack.length - 1];
87
+ if (top && top.key === key) {
88
+ top.before = mergeStates(before, top.before);
89
+ top.after = mergeStates(top.after, after);
90
+ } else {
91
+ this.undoStack.push({ label, key, before, after });
92
+ if (this.undoStack.length > UNDO_LIMIT) this.undoStack.shift();
93
+ }
94
+ this.redoStack = [];
95
+ }
96
+ /**
97
+ * Fold a write the app made on its own into the step the author took last, rather than recording a step of
98
+ * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its
99
+ * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is
100
+ * nothing to fold into, and the write simply stands.
101
+ */
102
+ fold(before, after) {
103
+ const top = this.undoStack[this.undoStack.length - 1];
104
+ if (!top) return;
105
+ const known = new Set(top.before.map((s) => s.path));
106
+ top.before = [...top.before, ...before.filter((s) => !known.has(s.path))];
107
+ top.after = mergeStates(top.after, after);
108
+ }
109
+ /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and
110
+ * checks drift itself). Most callers want `applyUndo`. */
111
+ undo() {
112
+ const entry = this.undoStack.pop();
113
+ if (!entry) return void 0;
114
+ this.redoStack.push(entry);
115
+ return entry.before;
116
+ }
117
+ /** Move the top redo entry to undo and return its after-states (see `undo`). */
118
+ redo() {
119
+ const entry = this.redoStack.pop();
120
+ if (!entry) return void 0;
121
+ this.undoStack.push(entry);
122
+ return entry.after;
123
+ }
124
+ /** An undo whose states could not be written: the step goes back where it was, so the author can try again
125
+ * once the file can be written. */
126
+ undoFailed() {
127
+ const entry = this.redoStack.pop();
128
+ if (entry) this.undoStack.push(entry);
129
+ }
130
+ /** The same for a redo that could not be written. */
131
+ redoFailed() {
132
+ const entry = this.undoStack.pop();
133
+ if (entry) this.redoStack.push(entry);
134
+ }
135
+ /**
136
+ * Undo the top step: check every file still says what the step left it saying, then write its befores. A
137
+ * file that has changed since refuses the undo, and the step is dropped with every step below it (they were
138
+ * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there
139
+ * is nothing to undo.
140
+ */
141
+ async applyUndo() {
142
+ return this.apply("undo");
143
+ }
144
+ /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */
145
+ async applyRedo() {
146
+ return this.apply("redo");
147
+ }
148
+ async apply(way) {
149
+ const from = way === "undo" ? this.undoStack : this.redoStack;
150
+ const entry = from[from.length - 1];
151
+ if (!entry) return void 0;
152
+ const expected = way === "undo" ? entry.after : entry.before;
153
+ const target = way === "undo" ? entry.before : entry.after;
154
+ const restorer = this.opts.restorer;
155
+ const current = new Map((this.opts.read ?? captureBefore)(target.map((s) => s.path)).map((s) => [s.path, s.content]));
156
+ const drifted = expected.filter((s) => {
157
+ if (s.local === true) return false;
158
+ const now = current.get(s.path) ?? null;
159
+ const r = restorer?.(s.path);
160
+ return r ? !r.same(s.content, now) : now !== s.content;
161
+ }).map((s) => s.path);
162
+ if (drifted.length > 0) {
163
+ from.length = 0;
164
+ const name = drifted.length === 1 ? `${path.basename(drifted[0])} has` : `${listNames(drifted)} have`;
165
+ return {
166
+ ok: false,
167
+ label: entry.label,
168
+ drift: drifted,
169
+ error: `couldn't ${way} "${entry.label}": ${name} changed since, so ${way === "undo" ? "undoing" : "redoing"} it would overwrite that. Nothing from that step ${way === "undo" ? "back" : "on"} can be ${way === "undo" ? "undone" : "redone"} now.`
170
+ };
171
+ }
172
+ const states = target.map((s) => {
173
+ const r = restorer?.(s.path);
174
+ return r ? { ...s, content: r.restore(s.content, current.get(s.path) ?? null) } : s;
175
+ });
176
+ from.pop();
177
+ (way === "undo" ? this.redoStack : this.undoStack).push(entry);
178
+ const applied = await (this.opts.write ?? applyStates)(states);
179
+ if (!applied.ok) {
180
+ if (way === "undo") this.undoFailed();
181
+ else this.redoFailed();
182
+ return { ok: false, label: entry.label, error: applied.error };
183
+ }
184
+ return { ok: true, label: entry.label };
185
+ }
186
+ };
187
+
188
+ exports.History = History;
189
+ exports.UNDO_LIMIT = UNDO_LIMIT;
190
+ exports.applyStates = applyStates;
191
+ exports.captureBefore = captureBefore;
192
+ exports.writeFailure = writeFailure;
193
+ //# sourceMappingURL=history.cjs.map
194
+ //# sourceMappingURL=history.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/history.ts"],"names":["existsSync","readFileSync","writeTextFilesAsync","deleteFileAsync","basename","rmSync","writeFileSync"],"mappings":"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","file":"history.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The file-byte history: undo and redo for writes an app makes to files, the\n// files-are-truth way. A step is a set of file-state transitions (a path's\n// content before and after, null meaning absent); undo writes the befores\n// back, redo the afters. No command log: the bytes on disk are the model, so\n// restoring bytes is undo.\n//\n// Storyletter's, lifted into the shell (patterkit design/patterpad-undo.md,\n// ruling 2) so Patterpad's project-level undo is the same code. The two apps'\n// DOCUMENT undo still shares nothing (Patterpad's open scene is ProseMirror's);\n// this is the file half only.\n//\n// Three things beyond Storyletter's first version:\n//\n// - DRIFT IS REFUSED (ruling 5). Before an undo or redo, each file must still\n// say what the step left it saying (its after, for an undo; its before, for a\n// redo). A file changed since, by a pull, a merge or another app, would have\n// that work overwritten, so the step is refused, naming the file, and dropped\n// with every step below it: none of them can now be applied safely.\n// - A PER-FILE RESTORE HOOK, for a file an app restores its own way (Patterpad\n// restores a `.patterx` block by block but stamps its edit trail afresh, which\n// moves on every save and so must not count as drift).\n// - THE WRITER IS THE APP'S. Patterpad writes through its own ordered queue;\n// `applyStates` (all or nothing through the VC layer) is the default.\n//\n// Main-process code: it reads and writes files.\n// ---------------------------------------------------------------------------\n\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { deleteFileAsync, writeTextFilesAsync } from \"@wildwinter/simple-vc-lib\";\nimport type { VCWriteOutcome } from \"@wildwinter/simple-vc-lib\";\n\n/** A file's content, or null when the file is absent (created / deleted). */\nexport interface FileState {\n path: string;\n content: string | null;\n /** Bookkeeping that lives beside the project but outside version control: restored with plain writes,\n * never checked out or added, once the versioned files have landed. */\n local?: boolean;\n}\n\n/** Read the current on-disk state of a set of paths. */\nexport function captureBefore(paths: string[]): FileState[] {\n return paths.map((path) => ({ path, content: existsSync(path) ? readFileSync(path, \"utf8\") : null }));\n}\n\n/** What applying a set of states came to. `wrote` says whether the writes landed before something else (a\n * delete) failed, which decides whether there is now a change on disk for the history to know about. */\nexport type Applied = { ok: true } | { ok: false; error: string; wrote: boolean };\n\n/** How a set of states is written: `applyStates`, or the app's own write path. */\nexport type StateWriter = (states: FileState[]) => Promise<Applied>;\n\n/**\n * Apply file states through the VC layer: write content, or delete when null.\n *\n * ALL OR NOTHING, and off the main thread (simple-vc-lib 0.5.0): every file is checked out first, and if any\n * one is refused nothing is written. The deletes run only once the whole batch has landed, so a rename whose\n * new path was refused never loses the old file.\n */\nexport async function applyStates(all: FileState[]): Promise<Applied> {\n const states = all.filter((s) => s.local !== true);\n const writes = states.filter((s) => s.content !== null).map((s) => ({ filePath: s.path, content: s.content! }));\n if (writes.length > 0) {\n try {\n const batch = await writeTextFilesAsync(writes, \"utf8\", { allOrNothing: true });\n if (!batch.success) return { ok: false, error: writeFailure(batch.results), wrote: false };\n } catch (e) {\n return { ok: false, error: `couldn't save: ${e instanceof Error ? e.message : String(e)}`, wrote: false };\n }\n }\n const undeleted: string[] = [];\n for (const s of states) {\n if (s.content !== null || !existsSync(s.path)) continue;\n try {\n const gone = await deleteFileAsync(s.path);\n if (!gone.success) undeleted.push(`${basename(s.path)} (${gone.message || gone.status})`);\n } catch (e) {\n undeleted.push(`${basename(s.path)} (${e instanceof Error ? e.message : String(e)})`);\n }\n }\n if (undeleted.length > 0) return { ok: false, error: `couldn't delete ${undeleted.join(\"; \")}`, wrote: writes.length > 0 };\n // The bookkeeping last, and only once the project's own files have landed, so it never says the project is\n // at a revision its files are not.\n for (const s of all) {\n if (s.local !== true) continue;\n if (s.content === null) rmSync(s.path, { force: true });\n else writeFileSync(s.path, s.content, \"utf8\");\n }\n return { ok: true };\n}\n\n/**\n * What a refused batch says: the file, and why. A refusal names who holds it (\"'docks.storyletdeck' is locked\n * by bob@bob-ws\"); every other file in an all-or-nothing batch reports only that it was not prepared because of\n * that one, which is not worth repeating.\n */\nexport function writeFailure(results: VCWriteOutcome[]): string {\n const failed = results.filter((r) => !r.success);\n const refused = failed.filter((r) => r.status === \"locked\" || r.status === \"outOfDate\");\n const say = (r: VCWriteOutcome): string => {\n const name = basename(r.filePath);\n const why = r.message || r.status;\n return why.includes(name) ? why : `${name} (${why})`;\n };\n if (refused.length > 0) return `nothing was saved: ${refused.map(say).join(\"; \")}`;\n return `couldn't save ${failed.map(say).join(\"; \") || \"the project\"}`;\n}\n\n/** How many steps Undo keeps: ProseMirror's default depth, the same in both apps. A step holds whole files\n * before and after, so an uncapped stack grew for as long as the window stayed open. */\nexport const UNDO_LIMIT = 100;\n\n/**\n * A file an app restores its own way. `same` says whether the file on disk still says what the step expects\n * (the drift check), and `restore` gives the content to write for the target, from what is on disk now. A\n * `.patterx` compares and restores every block but its edit trail, which the undo stamps afresh.\n */\nexport interface FileRestorer {\n same(expected: string | null, current: string | null): boolean;\n restore(target: string | null, current: string | null): string | null;\n}\n\nexport interface HistoryOptions {\n /** How an undo or redo is written. Default `applyStates`. */\n write?: StateWriter;\n /** A restorer for a file the app restores its own way, by path; undefined for every other file. */\n restorer?: (path: string) => FileRestorer | undefined;\n /** How each file's current bytes are read for the drift check. Default from disk (`captureBefore`). */\n read?: (paths: string[]) => FileState[];\n}\n\n/** What an undo or redo came to. `drift` names the files that changed since the step, when that refused it. */\nexport type StepOutcome =\n | { ok: true; label: string }\n | { ok: false; label: string; error: string; drift?: string[] };\n\ninterface Entry {\n label: string;\n key: string;\n before: FileState[];\n after: FileState[];\n}\n\n/** Merge state lists by path, `override` winning. */\nfunction mergeStates(base: FileState[], override: FileState[]): FileState[] {\n const byPath = new Map<string, FileState>();\n for (const s of base) byPath.set(s.path, s);\n for (const s of override) byPath.set(s.path, s);\n return [...byPath.values()];\n}\n\n/** File names, as a refusal lists them. */\nconst listNames = (paths: string[]): string => paths.map((p) => basename(p)).join(\", \");\n\nexport class History {\n private undoStack: Entry[] = [];\n private redoStack: Entry[] = [];\n private readonly opts: HistoryOptions;\n\n constructor(opts: HistoryOptions = {}) {\n this.opts = opts;\n }\n\n reset(): void {\n this.undoStack = [];\n this.redoStack = [];\n }\n\n get canUndo(): boolean { return this.undoStack.length > 0; }\n get canRedo(): boolean { return this.redoStack.length > 0; }\n /** The labels of the next undo and redo, for a menu (\"Undo Rename Card\"). */\n get undoLabel(): string | undefined { return this.undoStack[this.undoStack.length - 1]?.label; }\n get redoLabel(): string | undefined { return this.redoStack[this.redoStack.length - 1]?.label; }\n\n /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit\n * continuing, as typing into one field). A new distinct edit clears redo. */\n record(label: string, key: string, before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (top && top.key === key) {\n top.before = mergeStates(before, top.before); // keep the earliest before per path\n top.after = mergeStates(top.after, after); // keep the latest after per path\n } else {\n this.undoStack.push({ label, key, before, after });\n if (this.undoStack.length > UNDO_LIMIT) this.undoStack.shift();\n }\n this.redoStack = [];\n }\n\n /**\n * Fold a write the app made on its own into the step the author took last, rather than recording a step of\n * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its\n * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is\n * nothing to fold into, and the write simply stands.\n */\n fold(before: FileState[], after: FileState[]): void {\n const top = this.undoStack[this.undoStack.length - 1];\n if (!top) return;\n const known = new Set(top.before.map((s) => s.path));\n top.before = [...top.before, ...before.filter((s) => !known.has(s.path))];\n top.after = mergeStates(top.after, after);\n }\n\n /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and\n * checks drift itself). Most callers want `applyUndo`. */\n undo(): FileState[] | undefined {\n const entry = this.undoStack.pop();\n if (!entry) return undefined;\n this.redoStack.push(entry);\n return entry.before;\n }\n\n /** Move the top redo entry to undo and return its after-states (see `undo`). */\n redo(): FileState[] | undefined {\n const entry = this.redoStack.pop();\n if (!entry) return undefined;\n this.undoStack.push(entry);\n return entry.after;\n }\n\n /** An undo whose states could not be written: the step goes back where it was, so the author can try again\n * once the file can be written. */\n undoFailed(): void {\n const entry = this.redoStack.pop();\n if (entry) this.undoStack.push(entry);\n }\n\n /** The same for a redo that could not be written. */\n redoFailed(): void {\n const entry = this.undoStack.pop();\n if (entry) this.redoStack.push(entry);\n }\n\n /**\n * Undo the top step: check every file still says what the step left it saying, then write its befores. A\n * file that has changed since refuses the undo, and the step is dropped with every step below it (they were\n * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there\n * is nothing to undo.\n */\n async applyUndo(): Promise<StepOutcome | undefined> {\n return this.apply(\"undo\");\n }\n\n /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */\n async applyRedo(): Promise<StepOutcome | undefined> {\n return this.apply(\"redo\");\n }\n\n private async apply(way: \"undo\" | \"redo\"): Promise<StepOutcome | undefined> {\n const from = way === \"undo\" ? this.undoStack : this.redoStack;\n const entry = from[from.length - 1];\n if (!entry) return undefined;\n const expected = way === \"undo\" ? entry.after : entry.before;\n const target = way === \"undo\" ? entry.before : entry.after;\n const restorer = this.opts.restorer;\n const current = new Map((this.opts.read ?? captureBefore)(target.map((s) => s.path)).map((s) => [s.path, s.content]));\n\n // The drift check, over every file the step touches.\n const drifted = expected.filter((s) => {\n if (s.local === true) return false; // bookkeeping outside version control is the app's own\n const now = current.get(s.path) ?? null;\n const r = restorer?.(s.path);\n return r ? !r.same(s.content, now) : now !== s.content;\n }).map((s) => s.path);\n if (drifted.length > 0) {\n // Every step from here down was taken on top of what has changed: none can be applied safely.\n from.length = 0;\n const name = drifted.length === 1 ? `${basename(drifted[0]!)} has` : `${listNames(drifted)} have`;\n return {\n ok: false, label: entry.label, drift: drifted,\n error: `couldn't ${way} \"${entry.label}\": ${name} changed since, so ${way === \"undo\" ? \"undoing\" : \"redoing\"} it would overwrite that. `\n + `Nothing from that step ${way === \"undo\" ? \"back\" : \"on\"} can be ${way === \"undo\" ? \"undone\" : \"redone\"} now.`,\n };\n }\n\n const states = target.map((s) => {\n const r = restorer?.(s.path);\n return r ? { ...s, content: r.restore(s.content, current.get(s.path) ?? null) } : s;\n });\n from.pop();\n (way === \"undo\" ? this.redoStack : this.undoStack).push(entry);\n const applied = await (this.opts.write ?? applyStates)(states);\n if (!applied.ok) {\n if (way === \"undo\") this.undoFailed(); else this.redoFailed();\n return { ok: false, label: entry.label, error: applied.error };\n }\n return { ok: true, label: entry.label };\n }\n}\n"]}
@@ -0,0 +1,111 @@
1
+ import { VCWriteOutcome } from '@wildwinter/simple-vc-lib';
2
+
3
+ /** A file's content, or null when the file is absent (created / deleted). */
4
+ interface FileState {
5
+ path: string;
6
+ content: string | null;
7
+ /** Bookkeeping that lives beside the project but outside version control: restored with plain writes,
8
+ * never checked out or added, once the versioned files have landed. */
9
+ local?: boolean;
10
+ }
11
+ /** Read the current on-disk state of a set of paths. */
12
+ declare function captureBefore(paths: string[]): FileState[];
13
+ /** What applying a set of states came to. `wrote` says whether the writes landed before something else (a
14
+ * delete) failed, which decides whether there is now a change on disk for the history to know about. */
15
+ type Applied = {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ wrote: boolean;
21
+ };
22
+ /** How a set of states is written: `applyStates`, or the app's own write path. */
23
+ type StateWriter = (states: FileState[]) => Promise<Applied>;
24
+ /**
25
+ * Apply file states through the VC layer: write content, or delete when null.
26
+ *
27
+ * ALL OR NOTHING, and off the main thread (simple-vc-lib 0.5.0): every file is checked out first, and if any
28
+ * one is refused nothing is written. The deletes run only once the whole batch has landed, so a rename whose
29
+ * new path was refused never loses the old file.
30
+ */
31
+ declare function applyStates(all: FileState[]): Promise<Applied>;
32
+ /**
33
+ * What a refused batch says: the file, and why. A refusal names who holds it ("'docks.storyletdeck' is locked
34
+ * by bob@bob-ws"); every other file in an all-or-nothing batch reports only that it was not prepared because of
35
+ * that one, which is not worth repeating.
36
+ */
37
+ declare function writeFailure(results: VCWriteOutcome[]): string;
38
+ /** How many steps Undo keeps: ProseMirror's default depth, the same in both apps. A step holds whole files
39
+ * before and after, so an uncapped stack grew for as long as the window stayed open. */
40
+ declare const UNDO_LIMIT = 100;
41
+ /**
42
+ * A file an app restores its own way. `same` says whether the file on disk still says what the step expects
43
+ * (the drift check), and `restore` gives the content to write for the target, from what is on disk now. A
44
+ * `.patterx` compares and restores every block but its edit trail, which the undo stamps afresh.
45
+ */
46
+ interface FileRestorer {
47
+ same(expected: string | null, current: string | null): boolean;
48
+ restore(target: string | null, current: string | null): string | null;
49
+ }
50
+ interface HistoryOptions {
51
+ /** How an undo or redo is written. Default `applyStates`. */
52
+ write?: StateWriter;
53
+ /** A restorer for a file the app restores its own way, by path; undefined for every other file. */
54
+ restorer?: (path: string) => FileRestorer | undefined;
55
+ /** How each file's current bytes are read for the drift check. Default from disk (`captureBefore`). */
56
+ read?: (paths: string[]) => FileState[];
57
+ }
58
+ /** What an undo or redo came to. `drift` names the files that changed since the step, when that refused it. */
59
+ type StepOutcome = {
60
+ ok: true;
61
+ label: string;
62
+ } | {
63
+ ok: false;
64
+ label: string;
65
+ error: string;
66
+ drift?: string[];
67
+ };
68
+ declare class History {
69
+ private undoStack;
70
+ private redoStack;
71
+ private readonly opts;
72
+ constructor(opts?: HistoryOptions);
73
+ reset(): void;
74
+ get canUndo(): boolean;
75
+ get canRedo(): boolean;
76
+ /** The labels of the next undo and redo, for a menu ("Undo Rename Card"). */
77
+ get undoLabel(): string | undefined;
78
+ get redoLabel(): string | undefined;
79
+ /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit
80
+ * continuing, as typing into one field). A new distinct edit clears redo. */
81
+ record(label: string, key: string, before: FileState[], after: FileState[]): void;
82
+ /**
83
+ * Fold a write the app made on its own into the step the author took last, rather than recording a step of
84
+ * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its
85
+ * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is
86
+ * nothing to fold into, and the write simply stands.
87
+ */
88
+ fold(before: FileState[], after: FileState[]): void;
89
+ /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and
90
+ * checks drift itself). Most callers want `applyUndo`. */
91
+ undo(): FileState[] | undefined;
92
+ /** Move the top redo entry to undo and return its after-states (see `undo`). */
93
+ redo(): FileState[] | undefined;
94
+ /** An undo whose states could not be written: the step goes back where it was, so the author can try again
95
+ * once the file can be written. */
96
+ undoFailed(): void;
97
+ /** The same for a redo that could not be written. */
98
+ redoFailed(): void;
99
+ /**
100
+ * Undo the top step: check every file still says what the step left it saying, then write its befores. A
101
+ * file that has changed since refuses the undo, and the step is dropped with every step below it (they were
102
+ * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there
103
+ * is nothing to undo.
104
+ */
105
+ applyUndo(): Promise<StepOutcome | undefined>;
106
+ /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */
107
+ applyRedo(): Promise<StepOutcome | undefined>;
108
+ private apply;
109
+ }
110
+
111
+ export { type Applied, type FileRestorer, type FileState, History, type HistoryOptions, type StateWriter, type StepOutcome, UNDO_LIMIT, applyStates, captureBefore, writeFailure };
@@ -0,0 +1,111 @@
1
+ import { VCWriteOutcome } from '@wildwinter/simple-vc-lib';
2
+
3
+ /** A file's content, or null when the file is absent (created / deleted). */
4
+ interface FileState {
5
+ path: string;
6
+ content: string | null;
7
+ /** Bookkeeping that lives beside the project but outside version control: restored with plain writes,
8
+ * never checked out or added, once the versioned files have landed. */
9
+ local?: boolean;
10
+ }
11
+ /** Read the current on-disk state of a set of paths. */
12
+ declare function captureBefore(paths: string[]): FileState[];
13
+ /** What applying a set of states came to. `wrote` says whether the writes landed before something else (a
14
+ * delete) failed, which decides whether there is now a change on disk for the history to know about. */
15
+ type Applied = {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ wrote: boolean;
21
+ };
22
+ /** How a set of states is written: `applyStates`, or the app's own write path. */
23
+ type StateWriter = (states: FileState[]) => Promise<Applied>;
24
+ /**
25
+ * Apply file states through the VC layer: write content, or delete when null.
26
+ *
27
+ * ALL OR NOTHING, and off the main thread (simple-vc-lib 0.5.0): every file is checked out first, and if any
28
+ * one is refused nothing is written. The deletes run only once the whole batch has landed, so a rename whose
29
+ * new path was refused never loses the old file.
30
+ */
31
+ declare function applyStates(all: FileState[]): Promise<Applied>;
32
+ /**
33
+ * What a refused batch says: the file, and why. A refusal names who holds it ("'docks.storyletdeck' is locked
34
+ * by bob@bob-ws"); every other file in an all-or-nothing batch reports only that it was not prepared because of
35
+ * that one, which is not worth repeating.
36
+ */
37
+ declare function writeFailure(results: VCWriteOutcome[]): string;
38
+ /** How many steps Undo keeps: ProseMirror's default depth, the same in both apps. A step holds whole files
39
+ * before and after, so an uncapped stack grew for as long as the window stayed open. */
40
+ declare const UNDO_LIMIT = 100;
41
+ /**
42
+ * A file an app restores its own way. `same` says whether the file on disk still says what the step expects
43
+ * (the drift check), and `restore` gives the content to write for the target, from what is on disk now. A
44
+ * `.patterx` compares and restores every block but its edit trail, which the undo stamps afresh.
45
+ */
46
+ interface FileRestorer {
47
+ same(expected: string | null, current: string | null): boolean;
48
+ restore(target: string | null, current: string | null): string | null;
49
+ }
50
+ interface HistoryOptions {
51
+ /** How an undo or redo is written. Default `applyStates`. */
52
+ write?: StateWriter;
53
+ /** A restorer for a file the app restores its own way, by path; undefined for every other file. */
54
+ restorer?: (path: string) => FileRestorer | undefined;
55
+ /** How each file's current bytes are read for the drift check. Default from disk (`captureBefore`). */
56
+ read?: (paths: string[]) => FileState[];
57
+ }
58
+ /** What an undo or redo came to. `drift` names the files that changed since the step, when that refused it. */
59
+ type StepOutcome = {
60
+ ok: true;
61
+ label: string;
62
+ } | {
63
+ ok: false;
64
+ label: string;
65
+ error: string;
66
+ drift?: string[];
67
+ };
68
+ declare class History {
69
+ private undoStack;
70
+ private redoStack;
71
+ private readonly opts;
72
+ constructor(opts?: HistoryOptions);
73
+ reset(): void;
74
+ get canUndo(): boolean;
75
+ get canRedo(): boolean;
76
+ /** The labels of the next undo and redo, for a menu ("Undo Rename Card"). */
77
+ get undoLabel(): string | undefined;
78
+ get redoLabel(): string | undefined;
79
+ /** Record a committed change; coalesce with the top entry when the key matches (the same logical edit
80
+ * continuing, as typing into one field). A new distinct edit clears redo. */
81
+ record(label: string, key: string, before: FileState[], after: FileState[]): void;
82
+ /**
83
+ * Fold a write the app made on its own into the step the author took last, rather than recording a step of
84
+ * its own: for a write nobody asked for (a value stored the first time a view is drawn), which a step of its
85
+ * own would have the author's next Undo undo instead of what they meant. With nothing to undo yet, there is
86
+ * nothing to fold into, and the write simply stands.
87
+ */
88
+ fold(before: FileState[], after: FileState[]): void;
89
+ /** Move the top undo entry to redo and return its before-states, for a caller that writes them itself (and
90
+ * checks drift itself). Most callers want `applyUndo`. */
91
+ undo(): FileState[] | undefined;
92
+ /** Move the top redo entry to undo and return its after-states (see `undo`). */
93
+ redo(): FileState[] | undefined;
94
+ /** An undo whose states could not be written: the step goes back where it was, so the author can try again
95
+ * once the file can be written. */
96
+ undoFailed(): void;
97
+ /** The same for a redo that could not be written. */
98
+ redoFailed(): void;
99
+ /**
100
+ * Undo the top step: check every file still says what the step left it saying, then write its befores. A
101
+ * file that has changed since refuses the undo, and the step is dropped with every step below it (they were
102
+ * taken on top of what has now been overwritten). A write that fails puts the step back. Undefined when there
103
+ * is nothing to undo.
104
+ */
105
+ applyUndo(): Promise<StepOutcome | undefined>;
106
+ /** Redo the top undone step, by the same rules: drift drops it and every step above it on the redo stack. */
107
+ applyRedo(): Promise<StepOutcome | undefined>;
108
+ private apply;
109
+ }
110
+
111
+ export { type Applied, type FileRestorer, type FileState, History, type HistoryOptions, type StateWriter, type StepOutcome, UNDO_LIMIT, applyStates, captureBefore, writeFailure };