nucleus-core-ts 0.10.135 → 0.10.137

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/dist/.build-ok CHANGED
@@ -1 +1 @@
1
- 0.10.135
1
+ 0.10.137
@@ -357,6 +357,12 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
357
357
  */ const renderRevisionCard = (item)=>{
358
358
  const returned = returnedSentence(say, item);
359
359
  const confirming = withdrawingId === item.instance_id;
360
+ /*
361
+ * Round 1 of a chain that OPENS on the author's step — "the author confirms
362
+ * the draft, then the reviewers" — has sent nothing back. Measured on the
363
+ * running panel: the card said "returned to you for a correction" and
364
+ * offered "I fixed it, send again" for a record nobody had looked at yet.
365
+ */ const firstRound = (item.pass_no ?? 1) <= 1 && !item.sent_back;
360
366
  return /*#__PURE__*/ _jsxs("div", {
361
367
  className: "flex flex-wrap items-start justify-between gap-3",
362
368
  children: [
@@ -365,13 +371,14 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
365
371
  children: [
366
372
  /*#__PURE__*/ _jsx("p", {
367
373
  className: theme.pending.card.title,
368
- children: say.revisionCardTitle
374
+ children: firstRound ? say.revisionFirstTitle : say.revisionCardTitle
369
375
  }),
370
376
  /*#__PURE__*/ _jsxs("p", {
371
377
  className: theme.pending.card.subtitle,
372
378
  children: [
373
379
  say.pendingCardTitle(item.flow_name, item.step_order),
374
- item.step_name ? ` · ${item.step_name}` : null
380
+ item.step_name ? ` · ${item.step_name}` : null,
381
+ !firstRound && item.pass_no ? ` · ${say.pendingRound(item.pass_no, item.max_passes ?? item.pass_no)}` : null
375
382
  ]
376
383
  }),
377
384
  returned ? /*#__PURE__*/ _jsx("p", {
@@ -428,7 +435,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
428
435
  },
429
436
  "aria-describedby": `resubmit-hint-${item.instance_id}`,
430
437
  className: theme.pending.approveButton,
431
- children: say.resubmit
438
+ children: firstRound ? say.submitForReview : say.resubmit
432
439
  }),
433
440
  /*#__PURE__*/ _jsx("button", {
434
441
  type: "button",
@@ -446,7 +453,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
446
453
  /*#__PURE__*/ _jsx("p", {
447
454
  id: `resubmit-hint-${item.instance_id}`,
448
455
  className: theme.pending.card.decisionMeta,
449
- children: say.resubmitHint
456
+ children: firstRound ? say.submitForReviewHint : say.resubmitHint
450
457
  })
451
458
  ]
452
459
  })
@@ -467,6 +474,13 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
467
474
  className: theme.pending.card.subtitle,
468
475
  children: item.step_name
469
476
  }) : null,
477
+ item.sent_back?.reason ? /*#__PURE__*/ _jsxs("p", {
478
+ className: theme.pending.card.decisionMeta,
479
+ children: [
480
+ typeof item.pass_no === 'number' && typeof item.max_passes === 'number' ? `${say.pendingRound(item.pass_no, item.max_passes)} · ` : null,
481
+ say.previousRoundReason(item.sent_back.reason)
482
+ ]
483
+ }) : null,
470
484
  renderRecord(item)
471
485
  ]
472
486
  }),
@@ -316,6 +316,7 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
316
316
  * a ref here, because the render needs it too — to tell this editor's save
317
317
  * from one left behind — and a ref restarted at 0 on each mount.
318
318
  */ /** The latest readiness question; any older answer is about another graph. */ const feasibilityTicketRef = useRef(0);
319
+ /** The pending automatic re-check after an edit; see `scheduleFeasibility`. */ const feasibilityTimerRef = useRef(null);
319
320
  /*
320
321
  * The id a NEW flow is saved under, minted at its first save and reused
321
322
  * until one succeeds. It is not written to `selectedFlowId` before then:
@@ -756,22 +757,40 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
756
757
  }
757
758
  });
758
759
  });
760
+ /*
761
+ * Re-ask once the author PAUSES, with the graph they paused on.
762
+ *
763
+ * The answer was dropped on every edit and came back only on "Şimdi kontrol
764
+ * et" or a save, so adding a third step left the card blank ("henüz
765
+ * karşılaştırılmadı") exactly when it should have turned red (found by the
766
+ * re-check). A request per keystroke is not the fix either; one request
767
+ * 700ms after the last edit is. The ticket still discards an answer that
768
+ * lands after a newer edit.
769
+ */ const scheduleFeasibility = useEffectEvent(()=>{
770
+ if (feasibilityTimerRef.current) clearTimeout(feasibilityTimerRef.current);
771
+ feasibilityTimerRef.current = setTimeout(()=>{
772
+ feasibilityTimerRef.current = null;
773
+ checkFeasibility();
774
+ }, 700);
775
+ });
776
+ useEffect(()=>()=>{
777
+ if (feasibilityTimerRef.current) clearTimeout(feasibilityTimerRef.current);
778
+ }, []);
759
779
  const handleModel2Change = useEffectEvent((next, notes)=>{
760
780
  store.setModel2Graph(next);
761
781
  if (notes.length > 0) store.setModel2Notes(notes);
762
782
  store.markEdited();
763
- /*
764
- * The readiness answer is about a graph that no longer exists the moment an
765
- * edit lands, so it is dropped rather than left on screen. Re-asking on
766
- * every keystroke would be a request per character; the author asks again
767
- * with the button, and a save always re-asks on the server anyway.
768
- */ dropFeasibility();
783
+ // The old answer is about a graph that no longer exists: drop it now,
784
+ // re-ask when the author pauses.
785
+ dropFeasibility();
786
+ scheduleFeasibility();
769
787
  });
770
788
  const handleMaxPassesChange = useEffectEvent((value)=>{
771
789
  store.setMaxPasses(value);
772
790
  store.markEdited();
773
791
  // The round limit is part of the question the card answered.
774
792
  dropFeasibility();
793
+ scheduleFeasibility();
775
794
  });
776
795
  // ─── Flow details ─────────────────────────────────────────────
777
796
  //
@@ -443,6 +443,8 @@ export type VerificationFlowLabels = {
443
443
  reviewStartedNote: string;
444
444
  /** On the reviewer's card: which round, and what Reject will do. */
445
445
  pendingRound: (pass: number, max: number) => string;
446
+ /** On a reviewer's card in a later round: why the previous round was rejected. */
447
+ previousRoundReason: (reason: string) => string;
446
448
  pendingRejectSendsBack: (stepLabel: string) => string;
447
449
  pendingRejectEndsReview: string;
448
450
  pendingLastRoundWarning: string;
@@ -480,6 +482,10 @@ export type VerificationFlowLabels = {
480
482
  maxPassesOutOfRange: string;
481
483
  /** The author's correction card — a correction step, not a review. */
482
484
  revisionCardTitle: string;
485
+ /** The same card on round 1, before anything has been sent back. */
486
+ revisionFirstTitle: string;
487
+ submitForReview: string;
488
+ submitForReviewHint: string;
483
489
  returnedBecause: (reason: string) => string;
484
490
  returnedNoReason: string;
485
491
  resubmit: string;
@@ -235,7 +235,7 @@
235
235
  approvalModeAll: 'Everyone listed',
236
236
  approvalModeAllHint: 'Every person above must decide before the record moves on.',
237
237
  approvalModeAny: 'Any one of them',
238
- approvalModeAnyHint: 'The first decision closes the step, and the others are no longer asked.',
238
+ approvalModeAnyHint: 'The first person listed to finish closes the step, and the others are no longer asked. A role set to "every member" finishes only when all its members have signed.',
239
239
  outcomes: 'Where each answer sends it',
240
240
  onApproved: 'If approved',
241
241
  onApprovedNext: 'Go to the next step',
@@ -320,6 +320,7 @@
320
320
  startReviewFailed: 'The review could not be started.',
321
321
  reviewStartedNote: 'Sent for review.',
322
322
  pendingRound: (pass, max)=>`Round ${pass} (at most ${max})`,
323
+ previousRoundReason: (reason)=>`Rejected last round because: ${reason}`,
323
324
  pendingRejectSendsBack: (stepLabel)=>`Rejecting sends this back to ${stepLabel}.`,
324
325
  pendingRejectEndsReview: 'Rejecting ends this review.',
325
326
  pendingLastRoundWarning: 'This is the last round this flow allows, so rejecting now ends the review for good — the record will not go back to its author again.',
@@ -350,6 +351,9 @@
350
351
  liveFlowLocked: (n)=>`${n} review${n === 1 ? ' is' : 's are'} running under this flow, so its steps are locked. You can still rename steps and edit what they send.`,
351
352
  maxPassesOutOfRange: 'Between 1 and 20.',
352
353
  revisionCardTitle: 'Returned to you for a correction',
354
+ revisionFirstTitle: 'Check your record before the reviewers see it',
355
+ submitForReview: 'Send to the reviewers',
356
+ submitForReviewHint: 'Make any last changes on the record first; the reviewers read it as it is when you send it.',
353
357
  returnedBecause: (reason)=>`Why it came back: ${reason}`,
354
358
  returnedNoReason: 'The reviewer did not give a reason.',
355
359
  resubmit: 'Fixed — send back for review',
@@ -428,7 +432,12 @@ export function recipientTypeLabels(say) {
428
432
  // one, the engine's sentence when it does not, and a generic line only when
429
433
  // the engine sent nothing at all.
430
434
  export function refusalSentence(say, code, detail, fallback, generic) {
431
- const own = code ? say.flowRefusalByCode[code] : undefined;
435
+ /*
436
+ * Publish refuses with the READINESS codes too (a chain needing more people
437
+ * than the install has). A host that worded those only for the readiness
438
+ * card got the English sentence in the publish banner — measured by the
439
+ * re-check on the Turkish panel.
440
+ */ const own = code ? say.flowRefusalByCode[code] ?? say.feasibilityByCode[code] : undefined;
432
441
  if (own) return own(detail ?? {});
433
442
  const decision = code ? say.decisionRefusalByCode[code] : undefined;
434
443
  if (decision) return decision;
@@ -251,7 +251,9 @@ export function StepCard({ step, graph, isFirst, isLast, roles, users, triggerLa
251
251
  step_node_id: step.node_id
252
252
  },
253
253
  actions: stepActions,
254
- triggers: STEP_TRIGGERS,
254
+ // "The author sent it back" can only happen on the author's own
255
+ // correction step; offered elsewhere it saved and never fired.
256
+ triggers: (step.kind ?? 'approval') === 'revision' ? STEP_TRIGGERS : STEP_TRIGGERS.filter((t)=>t !== 'on_resubmitted'),
255
257
  triggerLabels: triggerLabels,
256
258
  recipientTypeLabels: recipientTypeLabels,
257
259
  channelLabels: channelLabels,
@@ -412,7 +412,15 @@ export function updateActionAt(graph, index, patch) {
412
412
  /** The steps an approval on `nodeId` may skip to: strictly later ones. */ export function approveTargets(graph, nodeId) {
413
413
  const step = (graph.steps ?? []).find((s)=>s.node_id === nodeId);
414
414
  if (!step) return [];
415
- return (graph.steps ?? []).filter((s)=>s.step_order > step.step_order).sort(byOrder);
415
+ const later = (graph.steps ?? []).filter((s)=>s.step_order > step.step_order).sort(byOrder);
416
+ if ((step.kind ?? 'approval') !== 'revision') return later;
417
+ /*
418
+ * A correction step may not jump OVER a reviewer. Two correction steps at
419
+ * the front with different jumps gave each round a different entry, and a
420
+ * reviewer walked in round 1 but jumped over in round 2 kept a signature on
421
+ * the old text (found by the re-check). Mirrors `revision_skips_reviewers`.
422
+ */ const firstReviewer = later.find((s)=>(s.kind ?? 'approval') !== 'revision');
423
+ return firstReviewer ? later.filter((s)=>s.step_order <= firstReviewer.step_order) : later;
416
424
  }
417
425
  /**
418
426
  * The approval outcomes the step card may OFFER for `nodeId`.
@@ -547,10 +555,12 @@ export const MAX_PASSES_MAX = 20;
547
555
  * first (found in the audit). Telling the author the two buttons do the same
548
556
  * thing there is telling them a choice that matters does not.
549
557
  */ export function approvalModeHasNoEffect(step) {
550
- const slots = step.assignees ?? [];
551
- if (slots.length !== 1) return false;
552
- const only = slots[0];
553
- return !(only?.verifier_type === 'role' && only.role_quorum === 'all');
558
+ /*
559
+ * The engine reads "any one of them" as any one SLOT, and a slot of "every
560
+ * member" is finished only when every member has signed (0.10.135). So with
561
+ * ONE slot the two modes are the same rule whatever its quorum — the
562
+ * exception this carried described the engine before that fix.
563
+ */ return (step.assignees ?? []).length === 1;
554
564
  }
555
565
  /**
556
566
  * The part of a step patch a live flow still accepts: its wording.
@@ -12,7 +12,11 @@
12
12
  */ const dark = {
13
13
  editor: {
14
14
  root: 'flex-1 min-h-0 flex flex-col bg-[#08080d]',
15
- scroll: 'flex-1 overflow-y-auto px-6 py-5',
15
+ // `relative`: the scroll box must be the positioning context. Without it the
16
+ // editor's `sr-only` labels (position: absolute) anchored to the page, not
17
+ // to this box, escaped its clipping and stretched the DOCUMENT to ~3000px —
18
+ // a second scrollbar, and the panel's sticky header sliding over Kaydet.
19
+ scroll: 'relative flex-1 overflow-y-auto px-6 py-5',
16
20
  header: 'flex-shrink-0 px-6 pt-5 pb-3 flex items-start justify-between gap-4',
17
21
  headerTitle: 'text-[13px] font-semibold text-white/90 tracking-tight',
18
22
  headerSubtitle: 'text-[11px] text-white/40 mt-1 leading-relaxed max-w-prose',
@@ -140,7 +144,11 @@
140
144
  const light = {
141
145
  editor: {
142
146
  root: 'flex-1 min-h-0 flex flex-col bg-slate-50',
143
- scroll: 'flex-1 overflow-y-auto px-6 py-5',
147
+ // `relative`: the scroll box must be the positioning context. Without it the
148
+ // editor's `sr-only` labels (position: absolute) anchored to the page, not
149
+ // to this box, escaped its clipping and stretched the DOCUMENT to ~3000px —
150
+ // a second scrollbar, and the panel's sticky header sliding over Kaydet.
151
+ scroll: 'relative flex-1 overflow-y-auto px-6 py-5',
144
152
  header: 'flex-shrink-0 px-6 pt-5 pb-3 flex items-start justify-between gap-4',
145
153
  headerTitle: 'text-[13px] font-semibold text-slate-800 tracking-tight',
146
154
  headerSubtitle: 'text-[11px] text-slate-500 mt-1 leading-relaxed max-w-prose',
@@ -47,7 +47,7 @@ export const verificationFlowPageTheme = {
47
47
  select: 'rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-[11px] text-white/70 outline-none focus:border-white/20'
48
48
  },
49
49
  palette: {
50
- container: 'flex-1 overflow-auto p-4',
50
+ container: 'relative flex-1 overflow-auto p-4',
51
51
  title: 'text-[10px] font-bold uppercase tracking-[0.15em] text-white/30 mb-3 px-1',
52
52
  item: 'flex items-start gap-3 rounded-xl p-3 mb-2 cursor-grab active:cursor-grabbing transition-all border border-transparent hover:border-white/[0.08]',
53
53
  itemIcon: 'w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0',
@@ -107,7 +107,7 @@ export const verificationFlowPageTheme = {
107
107
  header: 'flex items-center justify-between px-4 py-3 border-b border-white/[0.06]',
108
108
  headerTitle: 'text-[12px] font-semibold text-white/80',
109
109
  closeButton: 'w-6 h-6 rounded-md flex items-center justify-center text-white/30 hover:text-white/60 hover:bg-white/[0.06] transition-all',
110
- body: 'flex-1 overflow-auto p-4 space-y-5',
110
+ body: 'relative flex-1 overflow-auto p-4 space-y-5',
111
111
  help: 'rounded-lg bg-white/[0.04] px-3 py-2.5 text-[11px] leading-relaxed text-white/50',
112
112
  readOnlyRow: 'flex items-baseline justify-between gap-3 rounded-lg bg-white/[0.04] px-3 py-2 text-[11px] text-white/50',
113
113
  readOnlyValue: 'text-[12px] font-semibold tabular-nums text-white/80',
@@ -193,7 +193,7 @@ export const verificationFlowPageTheme = {
193
193
  }
194
194
  },
195
195
  flowList: {
196
- container: 'flex-1 overflow-auto p-6',
196
+ container: 'relative flex-1 overflow-auto p-6',
197
197
  grid: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto',
198
198
  createButton: 'group flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-white/[0.08] bg-white/[0.02] p-8 min-h-[180px] cursor-pointer hover:border-white/[0.15] hover:bg-white/[0.04] transition-all',
199
199
  createButtonIcon: 'text-white/20 group-hover:text-white/40 transition-colors mb-3',
@@ -274,7 +274,7 @@ export const verificationFlowPageLightTheme = {
274
274
  select: 'rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 text-[11px] text-gray-600 outline-none focus:border-gray-400'
275
275
  },
276
276
  palette: {
277
- container: 'flex-1 overflow-auto p-4',
277
+ container: 'relative flex-1 overflow-auto p-4',
278
278
  title: 'text-[10px] font-bold uppercase tracking-[0.15em] text-gray-400 mb-3 px-1',
279
279
  item: 'flex items-start gap-3 rounded-xl p-3 mb-2 cursor-grab active:cursor-grabbing transition-all border border-transparent hover:border-gray-200',
280
280
  itemIcon: 'w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0',
@@ -343,7 +343,7 @@ export const verificationFlowPageLightTheme = {
343
343
  header: 'flex items-center justify-between px-4 py-3 border-b border-gray-200',
344
344
  headerTitle: 'text-[12px] font-semibold text-gray-800',
345
345
  closeButton: 'w-6 h-6 rounded-md flex items-center justify-center text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-all',
346
- body: 'flex-1 overflow-auto p-4 space-y-5',
346
+ body: 'relative flex-1 overflow-auto p-4 space-y-5',
347
347
  help: 'rounded-lg bg-gray-50 border border-gray-200 px-3 py-2.5 text-[11px] leading-relaxed text-gray-600',
348
348
  readOnlyRow: 'flex items-baseline justify-between gap-3 rounded-lg bg-gray-50 border border-gray-200 px-3 py-2 text-[11px] text-gray-500',
349
349
  readOnlyValue: 'text-[12px] font-semibold tabular-nums text-gray-800',
@@ -429,7 +429,7 @@ export const verificationFlowPageLightTheme = {
429
429
  }
430
430
  },
431
431
  flowList: {
432
- container: 'flex-1 overflow-auto p-6',
432
+ container: 'relative flex-1 overflow-auto p-6',
433
433
  grid: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto',
434
434
  createButton: 'group flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 p-8 min-h-[180px] cursor-pointer hover:border-gray-400 hover:bg-gray-100 transition-all',
435
435
  createButtonIcon: 'text-gray-400 group-hover:text-gray-600 transition-colors mb-3',