@cosxai/ui 0.10.0 → 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.0",
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,10 +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. When active: filled accent circle;
464
- when inactive: outlined muted circle. Uses a small shield
465
- glyph so the affordance reads as "elevated privileges" at a
466
- glance, distinct from a normal action button. */}
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). */}
467
469
  {hasAdminItems && (
468
470
  <button
469
471
  type="button"
@@ -483,24 +485,28 @@ export function ActionBar({
483
485
  marginLeft: 2,
484
486
  marginRight: 2,
485
487
  borderRadius: 999,
486
- background: adminMode ? "var(--ck-accent, #4f46e5)" : "transparent",
488
+ background: "transparent",
487
489
  color: adminMode
488
- ? "var(--ck-accent-fg, #fff)"
490
+ ? "var(--ck-accent, #4f46e5)"
489
491
  : "var(--ck-text-secondary, #666)",
490
- border: adminMode
491
- ? "1px solid var(--ck-accent, #4f46e5)"
492
- : "1px solid var(--ck-border-subtle, #eee)",
492
+ border: "none",
493
493
  cursor: "pointer",
494
- transition: "background 160ms ease-out, color 160ms ease-out, border-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)",
495
501
  }}
496
502
  >
497
503
  <svg
498
- width="14"
499
- height="14"
504
+ width="16"
505
+ height="16"
500
506
  viewBox="0 0 24 24"
501
507
  fill="none"
502
508
  stroke="currentColor"
503
- strokeWidth="2"
509
+ strokeWidth="1.8"
504
510
  strokeLinecap="round"
505
511
  strokeLinejoin="round"
506
512
  aria-hidden
@@ -578,8 +584,16 @@ export function ActionBar({
578
584
  );
579
585
 
580
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;
581
595
  if (entry.kind === "flat" && entry.item) {
582
- return (
596
+ node = (
583
597
  <ActionBarButton
584
598
  key={entry.expansionKey}
585
599
  icon={entry.item.icon}
@@ -592,27 +606,42 @@ export function ActionBar({
592
606
  onClick={entry.item.onClick}
593
607
  />
594
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
+ );
595
631
  }
596
- const cat = entry.category!;
597
- const catDef = categories[cat];
598
- const hasActiveChild = entry.groupItems!.some((it) => it.active);
599
- 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.
600
637
  return (
601
- <ActionBarMenuGroup
602
- key={entry.expansionKey}
603
- label={catDef?.label ?? cat}
604
- icon={catDef?.icon}
605
- hasActiveChild={hasActiveChild}
606
- isOpen={isOpen}
607
- onToggle={() => setExpandedKey(isOpen ? null : entry.expansionKey)}
608
- items={entry.groupItems!}
609
- onItemClicked={(it) => {
610
- it.onClick();
611
- if (!it.keepGroupOpenOnClick) {
612
- setExpandedKey(null);
613
- }
614
- }}
615
- />
638
+ <span
639
+ key={`${entry.expansionKey}-admin`}
640
+ className="ck-actionbar-admin-item"
641
+ style={{ display: "inline-flex" }}
642
+ >
643
+ {node}
644
+ </span>
616
645
  );
617
646
  }
618
647
  }
@@ -661,17 +690,37 @@ function StatusDotGlyph({ color, pulse }: StatusDotGlyphProps) {
661
690
  );
662
691
  }
663
692
 
664
- // One global stylesheet entry for the pulse keyframe. Injected once
665
- // 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.
666
696
  if (typeof document !== "undefined") {
667
697
  const STYLE_ID = "ck-actionbar-status-pulse";
668
698
  if (!document.getElementById(STYLE_ID)) {
669
699
  const el = document.createElement("style");
670
700
  el.id = STYLE_ID;
671
- el.textContent = `@keyframes ck-actionbar-status-pulse {
701
+ el.textContent = `
702
+ @keyframes ck-actionbar-status-pulse {
672
703
  0%, 100% { opacity: 1; }
673
704
  50% { opacity: 0.45; }
674
- }`;
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
+ `;
675
724
  document.head.appendChild(el);
676
725
  }
677
726
  }