@cosxai/ui 0.10.1 → 0.10.2

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.2",
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",
@@ -412,7 +412,9 @@ export function ActionBar({
412
412
  boxShadow: "var(--ck-shadow-3)",
413
413
  fontFamily: "var(--ck-font-sans)",
414
414
  color: "var(--ck-text-primary)",
415
- transition: "background 180ms ease-out, border-color 180ms ease-out",
415
+ transition:
416
+ "background 260ms cubic-bezier(0.34, 1.56, 0.64, 1)," +
417
+ "border-color 260ms cubic-bezier(0.34, 1.56, 0.64, 1)",
416
418
  ...style,
417
419
  }}
418
420
  >
@@ -460,11 +462,10 @@ export function ActionBar({
460
462
  ) : null}
461
463
 
462
464
  {/* 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. */}
465
+ item has adminOnly=true. Borderless, transparent bg same
466
+ visual weight as the drag grip and the item icons around
467
+ it. Muted stroke by default, accent stroke + subtle scale
468
+ when active. Icon: shield (elevated privileges). */}
468
469
  {hasAdminItems && (
469
470
  <button
470
471
  type="button"
@@ -490,7 +491,13 @@ export function ActionBar({
490
491
  : "var(--ck-text-secondary, #666)",
491
492
  border: "none",
492
493
  cursor: "pointer",
493
- transition: "color 160ms ease-out",
494
+ transform: adminMode ? "scale(1.06)" : "scale(1)",
495
+ // Spring-ish easing so the tint + scale feel connected
496
+ // rather than mechanical — matches ck-actionbar-enter's
497
+ // curve for family consistency.
498
+ transition:
499
+ "color 260ms cubic-bezier(0.34, 1.56, 0.64, 1)," +
500
+ "transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1)",
494
501
  }}
495
502
  >
496
503
  <svg
@@ -504,14 +511,7 @@ export function ActionBar({
504
511
  strokeLinejoin="round"
505
512
  aria-hidden
506
513
  >
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" />
514
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
515
515
  </svg>
516
516
  </button>
517
517
  )}
@@ -584,8 +584,16 @@ export function ActionBar({
584
584
  );
585
585
 
586
586
  function renderEntry(entry: BuildEntry): ReactNode {
587
+ // Admin-only entries play a slide-in animation on mount so the
588
+ // reveal doesn't feel like items just popped in. Groups that
589
+ // contain any adminOnly items also get wrapped so the whole
590
+ // disclosure slides in together.
591
+ const isAdmin =
592
+ entry.item?.adminOnly === true ||
593
+ entry.groupItems?.some((it) => it.adminOnly === true) === true;
594
+ let node: ReactNode;
587
595
  if (entry.kind === "flat" && entry.item) {
588
- return (
596
+ node = (
589
597
  <ActionBarButton
590
598
  key={entry.expansionKey}
591
599
  icon={entry.item.icon}
@@ -598,27 +606,42 @@ export function ActionBar({
598
606
  onClick={entry.item.onClick}
599
607
  />
600
608
  );
609
+ } else {
610
+ const cat = entry.category!;
611
+ const catDef = categories[cat];
612
+ const hasActiveChild = entry.groupItems!.some((it) => it.active);
613
+ const isOpen = expandedKey === entry.expansionKey;
614
+ node = (
615
+ <ActionBarMenuGroup
616
+ key={entry.expansionKey}
617
+ label={catDef?.label ?? cat}
618
+ icon={catDef?.icon}
619
+ hasActiveChild={hasActiveChild}
620
+ isOpen={isOpen}
621
+ onToggle={() => setExpandedKey(isOpen ? null : entry.expansionKey)}
622
+ items={entry.groupItems!}
623
+ onItemClicked={(it) => {
624
+ it.onClick();
625
+ if (!it.keepGroupOpenOnClick) {
626
+ setExpandedKey(null);
627
+ }
628
+ }}
629
+ />
630
+ );
601
631
  }
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;
632
+ if (!isAdmin) return node;
633
+ // Wrapper carries the spring-in animation via keyframes injected
634
+ // at module load (see bottom of file). Key includes adminMode so
635
+ // React remounts the wrapper each time it flips, replaying the
636
+ // animation cleanly.
606
637
  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
- />
638
+ <span
639
+ key={`${entry.expansionKey}-admin`}
640
+ className="ck-actionbar-admin-item"
641
+ style={{ display: "inline-flex" }}
642
+ >
643
+ {node}
644
+ </span>
622
645
  );
623
646
  }
624
647
  }
@@ -667,17 +690,37 @@ function StatusDotGlyph({ color, pulse }: StatusDotGlyphProps) {
667
690
  );
668
691
  }
669
692
 
670
- // One global stylesheet entry for the pulse keyframe. Injected once
671
- // on first import. Idempotent — re-imports check by id.
693
+ // One global stylesheet entry for the pulse keyframe + admin-item
694
+ // entrance animation. Injected once on first import. Idempotent —
695
+ // re-imports check by id.
672
696
  if (typeof document !== "undefined") {
673
697
  const STYLE_ID = "ck-actionbar-status-pulse";
674
698
  if (!document.getElementById(STYLE_ID)) {
675
699
  const el = document.createElement("style");
676
700
  el.id = STYLE_ID;
677
- el.textContent = `@keyframes ck-actionbar-status-pulse {
701
+ el.textContent = `
702
+ @keyframes ck-actionbar-status-pulse {
678
703
  0%, 100% { opacity: 1; }
679
704
  50% { opacity: 0.45; }
680
- }`;
705
+ }
706
+ @keyframes ck-actionbar-admin-enter {
707
+ 0% {
708
+ opacity: 0;
709
+ transform: translateX(-6px) scale(0.92);
710
+ }
711
+ 60% {
712
+ opacity: 1;
713
+ }
714
+ 100% {
715
+ opacity: 1;
716
+ transform: translateX(0) scale(1);
717
+ }
718
+ }
719
+ .ck-actionbar-admin-item {
720
+ animation: ck-actionbar-admin-enter 260ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
721
+ transform-origin: left center;
722
+ }
723
+ `;
681
724
  document.head.appendChild(el);
682
725
  }
683
726
  }