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 +1 -1
- package/dist/fe/components/VerificationFlowPage/components/PendingTab.js +18 -4
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +25 -6
- package/dist/fe/components/VerificationFlowPage/labels.d.ts +6 -0
- package/dist/fe/components/VerificationFlowPage/labels.js +11 -2
- package/dist/fe/components/VerificationFlowPage/model2/StepCard.js +3 -1
- package/dist/fe/components/VerificationFlowPage/model2/editorGraph.js +15 -5
- package/dist/fe/components/VerificationFlowPage/model2/theme.js +10 -2
- package/dist/fe/components/VerificationFlowPage/theme/index.js +6 -6
- package/dist/index.js +1 -1
- package/dist/server.js +1 -1
- package/dist/src/Services/Verification/model2Validate.d.ts +1 -1
- package/package.json +1 -1
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.10.
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
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
|
|
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',
|