@cosxai/ui 0.10.1 → 0.10.3
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 +1 -1
- package/src/actionbar/ActionBar.tsx +94 -40
- package/src/actionbar/types.ts +12 -0
package/package.json
CHANGED
|
@@ -209,8 +209,19 @@ export function ActionBar({
|
|
|
209
209
|
});
|
|
210
210
|
}, [storageKey]);
|
|
211
211
|
const hasAdminItems = useMemo(() => items.some((it) => it.adminOnly === true), [items]);
|
|
212
|
+
// Filter semantics:
|
|
213
|
+
// admin OFF → hide items marked adminOnly (the toggle would reveal them)
|
|
214
|
+
// admin ON → hide items marked hiddenInAdmin (page opted them out of admin view)
|
|
215
|
+
// Together, adminOnly + hiddenInAdmin let pages choose:
|
|
216
|
+
// - additive: normal items unmarked → visible in both modes;
|
|
217
|
+
// admin items marked adminOnly → visible only in admin
|
|
218
|
+
// - exclusive: normal items marked hiddenInAdmin → hidden in admin;
|
|
219
|
+
// admin items marked adminOnly → visible only in admin
|
|
212
220
|
const visibleItems = useMemo(
|
|
213
|
-
() =>
|
|
221
|
+
() =>
|
|
222
|
+
adminMode
|
|
223
|
+
? items.filter((it) => it.hiddenInAdmin !== true)
|
|
224
|
+
: items.filter((it) => it.adminOnly !== true),
|
|
214
225
|
[items, adminMode],
|
|
215
226
|
);
|
|
216
227
|
|
|
@@ -412,7 +423,9 @@ export function ActionBar({
|
|
|
412
423
|
boxShadow: "var(--ck-shadow-3)",
|
|
413
424
|
fontFamily: "var(--ck-font-sans)",
|
|
414
425
|
color: "var(--ck-text-primary)",
|
|
415
|
-
transition:
|
|
426
|
+
transition:
|
|
427
|
+
"background 260ms cubic-bezier(0.34, 1.56, 0.64, 1)," +
|
|
428
|
+
"border-color 260ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
416
429
|
...style,
|
|
417
430
|
}}
|
|
418
431
|
>
|
|
@@ -460,11 +473,10 @@ export function ActionBar({
|
|
|
460
473
|
) : null}
|
|
461
474
|
|
|
462
475
|
{/* Admin-mode toggle. Only renders when at least one registered
|
|
463
|
-
item has adminOnly=true.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
Share / etc. */}
|
|
476
|
+
item has adminOnly=true. Borderless, transparent bg — same
|
|
477
|
+
visual weight as the drag grip and the item icons around
|
|
478
|
+
it. Muted stroke by default, accent stroke + subtle scale
|
|
479
|
+
when active. Icon: shield (elevated privileges). */}
|
|
468
480
|
{hasAdminItems && (
|
|
469
481
|
<button
|
|
470
482
|
type="button"
|
|
@@ -490,7 +502,13 @@ export function ActionBar({
|
|
|
490
502
|
: "var(--ck-text-secondary, #666)",
|
|
491
503
|
border: "none",
|
|
492
504
|
cursor: "pointer",
|
|
493
|
-
|
|
505
|
+
transform: adminMode ? "scale(1.06)" : "scale(1)",
|
|
506
|
+
// Spring-ish easing so the tint + scale feel connected
|
|
507
|
+
// rather than mechanical — matches ck-actionbar-enter's
|
|
508
|
+
// curve for family consistency.
|
|
509
|
+
transition:
|
|
510
|
+
"color 260ms cubic-bezier(0.34, 1.56, 0.64, 1)," +
|
|
511
|
+
"transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
494
512
|
}}
|
|
495
513
|
>
|
|
496
514
|
<svg
|
|
@@ -504,14 +522,7 @@ export function ActionBar({
|
|
|
504
522
|
strokeLinejoin="round"
|
|
505
523
|
aria-hidden
|
|
506
524
|
>
|
|
507
|
-
|
|
508
|
-
which is exactly what admin mode does. Two horizontal
|
|
509
|
-
tracks with a knob on each; the active vs. inactive
|
|
510
|
-
distinction comes from stroke colour, not fill. */}
|
|
511
|
-
<line x1="4" y1="8" x2="20" y2="8" />
|
|
512
|
-
<line x1="4" y1="16" x2="20" y2="16" />
|
|
513
|
-
<circle cx="10" cy="8" r="2" fill="currentColor" />
|
|
514
|
-
<circle cx="15" cy="16" r="2" fill="currentColor" />
|
|
525
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
515
526
|
</svg>
|
|
516
527
|
</button>
|
|
517
528
|
)}
|
|
@@ -584,8 +595,16 @@ export function ActionBar({
|
|
|
584
595
|
);
|
|
585
596
|
|
|
586
597
|
function renderEntry(entry: BuildEntry): ReactNode {
|
|
598
|
+
// Admin-only entries play a slide-in animation on mount so the
|
|
599
|
+
// reveal doesn't feel like items just popped in. Groups that
|
|
600
|
+
// contain any adminOnly items also get wrapped so the whole
|
|
601
|
+
// disclosure slides in together.
|
|
602
|
+
const isAdmin =
|
|
603
|
+
entry.item?.adminOnly === true ||
|
|
604
|
+
entry.groupItems?.some((it) => it.adminOnly === true) === true;
|
|
605
|
+
let node: ReactNode;
|
|
587
606
|
if (entry.kind === "flat" && entry.item) {
|
|
588
|
-
|
|
607
|
+
node = (
|
|
589
608
|
<ActionBarButton
|
|
590
609
|
key={entry.expansionKey}
|
|
591
610
|
icon={entry.item.icon}
|
|
@@ -598,27 +617,42 @@ export function ActionBar({
|
|
|
598
617
|
onClick={entry.item.onClick}
|
|
599
618
|
/>
|
|
600
619
|
);
|
|
620
|
+
} else {
|
|
621
|
+
const cat = entry.category!;
|
|
622
|
+
const catDef = categories[cat];
|
|
623
|
+
const hasActiveChild = entry.groupItems!.some((it) => it.active);
|
|
624
|
+
const isOpen = expandedKey === entry.expansionKey;
|
|
625
|
+
node = (
|
|
626
|
+
<ActionBarMenuGroup
|
|
627
|
+
key={entry.expansionKey}
|
|
628
|
+
label={catDef?.label ?? cat}
|
|
629
|
+
icon={catDef?.icon}
|
|
630
|
+
hasActiveChild={hasActiveChild}
|
|
631
|
+
isOpen={isOpen}
|
|
632
|
+
onToggle={() => setExpandedKey(isOpen ? null : entry.expansionKey)}
|
|
633
|
+
items={entry.groupItems!}
|
|
634
|
+
onItemClicked={(it) => {
|
|
635
|
+
it.onClick();
|
|
636
|
+
if (!it.keepGroupOpenOnClick) {
|
|
637
|
+
setExpandedKey(null);
|
|
638
|
+
}
|
|
639
|
+
}}
|
|
640
|
+
/>
|
|
641
|
+
);
|
|
601
642
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
643
|
+
if (!isAdmin) return node;
|
|
644
|
+
// Wrapper carries the spring-in animation via keyframes injected
|
|
645
|
+
// at module load (see bottom of file). Key includes adminMode so
|
|
646
|
+
// React remounts the wrapper each time it flips, replaying the
|
|
647
|
+
// animation cleanly.
|
|
606
648
|
return (
|
|
607
|
-
<
|
|
608
|
-
key={entry.expansionKey}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
items={entry.groupItems!}
|
|
615
|
-
onItemClicked={(it) => {
|
|
616
|
-
it.onClick();
|
|
617
|
-
if (!it.keepGroupOpenOnClick) {
|
|
618
|
-
setExpandedKey(null);
|
|
619
|
-
}
|
|
620
|
-
}}
|
|
621
|
-
/>
|
|
649
|
+
<span
|
|
650
|
+
key={`${entry.expansionKey}-admin`}
|
|
651
|
+
className="ck-actionbar-admin-item"
|
|
652
|
+
style={{ display: "inline-flex" }}
|
|
653
|
+
>
|
|
654
|
+
{node}
|
|
655
|
+
</span>
|
|
622
656
|
);
|
|
623
657
|
}
|
|
624
658
|
}
|
|
@@ -667,17 +701,37 @@ function StatusDotGlyph({ color, pulse }: StatusDotGlyphProps) {
|
|
|
667
701
|
);
|
|
668
702
|
}
|
|
669
703
|
|
|
670
|
-
// One global stylesheet entry for the pulse keyframe
|
|
671
|
-
// on first import. Idempotent —
|
|
704
|
+
// One global stylesheet entry for the pulse keyframe + admin-item
|
|
705
|
+
// entrance animation. Injected once on first import. Idempotent —
|
|
706
|
+
// re-imports check by id.
|
|
672
707
|
if (typeof document !== "undefined") {
|
|
673
708
|
const STYLE_ID = "ck-actionbar-status-pulse";
|
|
674
709
|
if (!document.getElementById(STYLE_ID)) {
|
|
675
710
|
const el = document.createElement("style");
|
|
676
711
|
el.id = STYLE_ID;
|
|
677
|
-
el.textContent =
|
|
712
|
+
el.textContent = `
|
|
713
|
+
@keyframes ck-actionbar-status-pulse {
|
|
678
714
|
0%, 100% { opacity: 1; }
|
|
679
715
|
50% { opacity: 0.45; }
|
|
680
|
-
}
|
|
716
|
+
}
|
|
717
|
+
@keyframes ck-actionbar-admin-enter {
|
|
718
|
+
0% {
|
|
719
|
+
opacity: 0;
|
|
720
|
+
transform: translateX(-6px) scale(0.92);
|
|
721
|
+
}
|
|
722
|
+
60% {
|
|
723
|
+
opacity: 1;
|
|
724
|
+
}
|
|
725
|
+
100% {
|
|
726
|
+
opacity: 1;
|
|
727
|
+
transform: translateX(0) scale(1);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
.ck-actionbar-admin-item {
|
|
731
|
+
animation: ck-actionbar-admin-enter 260ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
|
732
|
+
transform-origin: left center;
|
|
733
|
+
}
|
|
734
|
+
`;
|
|
681
735
|
document.head.appendChild(el);
|
|
682
736
|
}
|
|
683
737
|
}
|
package/src/actionbar/types.ts
CHANGED
|
@@ -60,6 +60,18 @@ export interface ActionBarItem {
|
|
|
60
60
|
// (or an equivalent gate) === true. The kit does not check
|
|
61
61
|
// permissions; it only handles the toggle UX.
|
|
62
62
|
adminOnly?: boolean;
|
|
63
|
+
// Marks a normally-visible item as hidden when admin mode is on.
|
|
64
|
+
// Symmetric counterpart to `adminOnly`: pages that want an
|
|
65
|
+
// exclusive "different toolset" UX (e.g. Share disappears in favour
|
|
66
|
+
// of Manage Share + Activity + History) mark their regular items
|
|
67
|
+
// hiddenInAdmin=true so they clear when the toggle flips on.
|
|
68
|
+
// Pages that want additive layering (regular items stay + admin
|
|
69
|
+
// items appear alongside) just leave this unset.
|
|
70
|
+
//
|
|
71
|
+
// Per-item because different pages within the same app want
|
|
72
|
+
// different behaviour: block_doc viewer wants exclusive; a
|
|
73
|
+
// hypothetical PDF admin surface might want additive.
|
|
74
|
+
hiddenInAdmin?: boolean;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
// Category definition — declared at the provider level. Drives
|