@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "COSX design system — React 19 component primitives shared across product-meta and other consumers",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -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
- () => (adminMode ? items : items.filter((it) => it.adminOnly !== true)),
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: "background 180ms ease-out, border-color 180ms ease-out",
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. Styled to match the grip: no border,
464
- transparent background, icon-only. Muted stroke by default,
465
- accent when active same visual weight as the other bar
466
- items so the affordance sits comfortably next to Theme /
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
- transition: "color 160ms ease-out",
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
- {/* Sliders/tuning icon reads as "surface more controls",
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
- return (
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
- const cat = entry.category!;
603
- const catDef = categories[cat];
604
- const hasActiveChild = entry.groupItems!.some((it) => it.active);
605
- const isOpen = expandedKey === entry.expansionKey;
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
- <ActionBarMenuGroup
608
- key={entry.expansionKey}
609
- label={catDef?.label ?? cat}
610
- icon={catDef?.icon}
611
- hasActiveChild={hasActiveChild}
612
- isOpen={isOpen}
613
- onToggle={() => setExpandedKey(isOpen ? null : entry.expansionKey)}
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. Injected once
671
- // on first import. Idempotent — re-imports check by id.
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 = `@keyframes ck-actionbar-status-pulse {
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
  }
@@ -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