@ssobig/writer-cli 0.3.4 → 0.3.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssobig/writer-cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Official agent CLI for SSOBIG WRITER",
5
5
  "type": "commonjs",
6
6
  "license": "UNLICENSED",
@@ -296,7 +296,7 @@
296
296
  return true;
297
297
  }
298
298
 
299
- return Object.freeze({
299
+ const mutations = {
300
300
  markChange,
301
301
  replaceData,
302
302
  arrayValue,
@@ -317,8 +317,55 @@
317
317
  characterSectionMove,
318
318
  commonAdd,
319
319
  toggleComponentField
320
- });
320
+ };
321
+ // Session-only history belongs to each Data Component, never browser storage.
322
+ const histories = new Map();
323
+ const historyFor = instance => {
324
+ if (!histories.has(instance.instanceId)) histories.set(instance.instanceId, { undo:[], redo:[] });
325
+ return histories.get(instance.instanceId);
326
+ };
327
+ const serialized = instance => JSON.stringify(options.draft(instance));
328
+ const wrapped = Object.fromEntries(Object.entries(mutations).map(([name, operation]) => [name,
329
+ name === "arrayValue" ? operation : (instance, ...args) => {
330
+ if (options.saveState?.(instance)?.locked) return false;
331
+ const before = serialized(instance), history = historyFor(instance);
332
+ const last = history.undo.at(-1);
333
+ if ((last && last.after !== before) || (history.redo.length && history.redo.at(-1).before !== before)) { history.undo=[]; history.redo=[]; }
334
+ const result = operation(instance, ...args);
335
+ const after = serialized(instance);
336
+ if (before === after) return result;
337
+ const now = Date.now(), key = name === "markChange" ? JSON.stringify(args[0]) : null;
338
+ const previous = history.undo.at(-1);
339
+ if (key && previous?.key === key && now-previous.time<800 && !history.redo.length) {
340
+ previous.after=after; previous.time=now;
341
+ } else history.undo.push({before,after,key,time:now});
342
+ history.redo=[];
343
+ while (history.undo.length>50 || (history.undo.length>1 && history.undo.reduce((n,x)=>n+x.before.length+x.after.length,0)>8*1024*1024)) history.undo.shift();
344
+ return result;
345
+ }
346
+ ]));
347
+ function restore(instance, direction) {
348
+ if (options.saveState?.(instance)?.locked) return false;
349
+ const history = historyFor(instance), source = history[direction], entry = source.at(-1);
350
+ if (!entry) return false;
351
+ const undo = direction === "undo";
352
+ if (serialized(instance) !== (undo ? entry.after : entry.before)) { histories.delete(instance.instanceId); return false; }
353
+ const nextData = JSON.parse(undo ? entry.before : entry.after);
354
+ try { options.validate?.(instance, nextData); } catch { histories.delete(instance.instanceId); return false; }
355
+ // Use the same autosave/CAS path as every other edit.
356
+ source.pop(); history[undo ? "redo" : "undo"].push(entry);
357
+ return replaceData(instance, nextData);
358
+ }
359
+ return Object.freeze({ ...wrapped, undo:instance=>restore(instance,"undo"), redo:instance=>restore(instance,"redo"), clearHistory:()=>histories.clear() });
321
360
  }
322
361
 
323
- return Object.freeze({ create });
362
+ function historyShortcut(event) {
363
+ if (event.defaultPrevented || event.isComposing || event.altKey || !(event.ctrlKey || event.metaKey)) return null;
364
+ if (event.target?.closest?.("input,textarea,select,[contenteditable]:not([contenteditable='false']),.cm-editor,dialog")) return null;
365
+ const key = String(event.key || "").toLowerCase();
366
+ if (key === "z") return event.shiftKey ? "redo" : "undo";
367
+ if (key === "y" && !event.shiftKey) return "redo";
368
+ return null;
369
+ }
370
+ return Object.freeze({ create, historyShortcut });
324
371
  });
@@ -322,7 +322,7 @@
322
322
  placeId: field("nullable-uuid"), characterId: field("nullable-stable-key")
323
323
  }, { timeRange: field("object", { item: "range" }), included: field("boolean") }),
324
324
  range: shape({ start: field("object", { item: "point" }), end: field("object", { item: "point" }) }),
325
- point: shape({ day: field("integer", { minimum: 0, maximum: 999999 }), hour: field("integer", { minimum: 0, maximum: 23 }), minute: field("integer", { minimum: 0, maximum: 59 }) })
325
+ point: shape({ day: field("integer", { minimum: -999999, maximum: 999999 }), hour: field("integer", { minimum: 0, maximum: 23 }), minute: field("integer", { minimum: 0, maximum: 59 }) })
326
326
  }
327
327
  });
328
328
  function eventTimelineError(data) {
@@ -262,9 +262,10 @@
262
262
  }
263
263
 
264
264
  function pointMinutes(point) { return point.day * 1440 + point.hour * 60 + point.minute; }
265
- function minutePoint(value) { return { day: Math.floor(value / 1440), hour: Math.floor(value % 1440 / 60), minute: value % 60 }; }
265
+ function minutePoint(value) { return { day: Math.floor(value / 1440), hour: Math.floor(((value % 1440 + 1440) % 1440) / 60), minute: (value % 60 + 60) % 60 }; }
266
266
  function defaultRange(start = { day: 0, hour: 9, minute: 0 }) { return { start: { ...start }, end: minutePoint(pointMinutes(start) + 30) }; }
267
- function pointLabel(point) { return `${point.day + 1}일차 ${String(point.hour).padStart(2, "0")}:${String(point.minute).padStart(2, "0")}`; }
267
+ function dayLabel(day) { return day === 0 ? "D-day" : `D${day > 0 ? "+" : ""}${day}`; }
268
+ function pointLabel(point) { return `${dayLabel(point.day)} ${String(point.hour).padStart(2, "0")}:${String(point.minute).padStart(2, "0")}`; }
268
269
  function rangeLabel(range) { return range ? `${pointLabel(range.start)}–${pointLabel(range.end)}` : ""; }
269
270
  function orderedEvents(data) {
270
271
  const groups = new Map(data.timeGroups.map((group, index) => [group.id, { ...group, index }]));
@@ -292,5 +293,5 @@
292
293
  throw new Error("타임라인 data가 등록된 모양이 아닙니다.");
293
294
  }
294
295
 
295
- return Object.freeze({ convertToEventTimeline, pointMinutes, minutePoint, defaultRange, pointLabel, rangeLabel, orderedEvents, eventTimelineProjection, detectShape, convertLegacyTimeline, compareLegacyToV2, normalizedTimelineData, entryPlaceNames, orderedEntries, overlapGroups, deriveClusters });
296
+ return Object.freeze({ convertToEventTimeline, pointMinutes, minutePoint, defaultRange, dayLabel, pointLabel, rangeLabel, orderedEvents, eventTimelineProjection, detectShape, convertLegacyTimeline, compareLegacyToV2, normalizedTimelineData, entryPlaceNames, orderedEntries, overlapGroups, deriveClusters });
296
297
  });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ssobig/writer-cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ssobig/writer-cli",
9
- "version": "0.3.4",
9
+ "version": "0.3.5",
10
10
  "dependencies": {
11
11
  "@supabase/supabase-js": "2.110.9"
12
12
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssobig/writer-cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "private": true,
5
5
  "description": "Internal agent-safe CLI for SSOBIG WRITER manuscript and asset operations",
6
6
  "type": "commonjs",