@cosxai/ui 0.9.0 → 0.10.0

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.9.0",
3
+ "version": "0.10.0",
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",
@@ -78,6 +78,25 @@ function loadCollapsed(storageKey: string): boolean {
78
78
  }
79
79
  }
80
80
 
81
+ function loadAdminMode(storageKey: string): boolean {
82
+ if (typeof window === "undefined") return false;
83
+ try {
84
+ return window.localStorage.getItem(`${storageKey}:admin`) === "1";
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function persistAdminMode(storageKey: string, on: boolean) {
91
+ if (typeof window === "undefined") return;
92
+ try {
93
+ if (on) window.localStorage.setItem(`${storageKey}:admin`, "1");
94
+ else window.localStorage.removeItem(`${storageKey}:admin`);
95
+ } catch {
96
+ /* ignore */
97
+ }
98
+ }
99
+
81
100
  interface BuildEntry {
82
101
  kind: "flat" | "group";
83
102
  expansionKey: string;
@@ -175,6 +194,26 @@ export function ActionBar({
175
194
  return () => window.removeEventListener("resize", reclamp);
176
195
  }, []);
177
196
 
197
+ // ----- Admin mode toggle (any registered item with adminOnly=true) -----
198
+ // Admin items stay hidden by default; a small toggle button (rendered
199
+ // between the grip and the first item) flips this to reveal them.
200
+ // Persisted per storageKey so admin users stay in the mode they
201
+ // last chose across route navigations. When no consumer registers
202
+ // admin items, the toggle button doesn't render.
203
+ const [adminMode, setAdminMode] = useState<boolean>(() => loadAdminMode(storageKey));
204
+ const toggleAdminMode = useCallback(() => {
205
+ setAdminMode((prev) => {
206
+ const next = !prev;
207
+ persistAdminMode(storageKey, next);
208
+ return next;
209
+ });
210
+ }, [storageKey]);
211
+ const hasAdminItems = useMemo(() => items.some((it) => it.adminOnly === true), [items]);
212
+ const visibleItems = useMemo(
213
+ () => (adminMode ? items : items.filter((it) => it.adminOnly !== true)),
214
+ [items, adminMode],
215
+ );
216
+
178
217
  // ----- Collapsed state (phone only) -----
179
218
  const [collapsed, setCollapsed] = useState<boolean>(() => loadCollapsed(storageKey));
180
219
  useEffect(() => {
@@ -241,14 +280,14 @@ export function ActionBar({
241
280
  const { leadingEntries, trailingEntries } = useMemo(() => {
242
281
  const leading: ActionBarItem[] = [];
243
282
  const trailing: ActionBarItem[] = [];
244
- for (const it of items) {
283
+ for (const it of visibleItems) {
245
284
  (it.slot === "trailing" ? trailing : leading).push(it);
246
285
  }
247
286
  return {
248
287
  leadingEntries: buildEntries(leading),
249
288
  trailingEntries: buildEntries(trailing),
250
289
  };
251
- }, [items]);
290
+ }, [visibleItems]);
252
291
  const entries = useMemo(
253
292
  () => [...leadingEntries, ...trailingEntries],
254
293
  [leadingEntries, trailingEntries],
@@ -359,12 +398,21 @@ export function ActionBar({
359
398
  alignItems: "center",
360
399
  gap: 4,
361
400
  zIndex: 80,
362
- background: "var(--ck-bg-surface)",
363
- border: "1px solid var(--ck-border-subtle)",
401
+ // Subtle accent tint when admin mode is active so the bar reads
402
+ // as "elevated privileges on" without a full colour swap.
403
+ background: adminMode
404
+ ? "color-mix(in oklab, var(--ck-accent, #4f46e5) 8%, var(--ck-bg-surface))"
405
+ : "var(--ck-bg-surface)",
406
+ border: `1px solid ${
407
+ adminMode
408
+ ? "var(--ck-accent, #4f46e5)"
409
+ : "var(--ck-border-subtle)"
410
+ }`,
364
411
  borderRadius: 999,
365
412
  boxShadow: "var(--ck-shadow-3)",
366
413
  fontFamily: "var(--ck-font-sans)",
367
414
  color: "var(--ck-text-primary)",
415
+ transition: "background 180ms ease-out, border-color 180ms ease-out",
368
416
  ...style,
369
417
  }}
370
418
  >
@@ -411,6 +459,57 @@ export function ActionBar({
411
459
  </button>
412
460
  ) : null}
413
461
 
462
+ {/* 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. */}
467
+ {hasAdminItems && (
468
+ <button
469
+ type="button"
470
+ onClick={toggleAdminMode}
471
+ aria-label={adminMode ? "Exit admin mode" : "Enter admin mode"}
472
+ aria-pressed={adminMode}
473
+ title={adminMode ? "Admin mode · click to exit" : "Enter admin mode"}
474
+ data-ck-actionbar-admin-toggle
475
+ data-active={adminMode ? "1" : "0"}
476
+ style={{
477
+ display: "inline-flex",
478
+ alignItems: "center",
479
+ justifyContent: "center",
480
+ width: 28,
481
+ height: 28,
482
+ padding: 0,
483
+ marginLeft: 2,
484
+ marginRight: 2,
485
+ borderRadius: 999,
486
+ background: adminMode ? "var(--ck-accent, #4f46e5)" : "transparent",
487
+ color: adminMode
488
+ ? "var(--ck-accent-fg, #fff)"
489
+ : "var(--ck-text-secondary, #666)",
490
+ border: adminMode
491
+ ? "1px solid var(--ck-accent, #4f46e5)"
492
+ : "1px solid var(--ck-border-subtle, #eee)",
493
+ cursor: "pointer",
494
+ transition: "background 160ms ease-out, color 160ms ease-out, border-color 160ms ease-out",
495
+ }}
496
+ >
497
+ <svg
498
+ width="14"
499
+ height="14"
500
+ viewBox="0 0 24 24"
501
+ fill="none"
502
+ stroke="currentColor"
503
+ strokeWidth="2"
504
+ strokeLinecap="round"
505
+ strokeLinejoin="round"
506
+ aria-hidden
507
+ >
508
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
509
+ </svg>
510
+ </button>
511
+ )}
512
+
414
513
  {/* Balancing leading spacer — only present when the right side
415
514
  holds ONLY a status dot (no trailing items). With a left
416
515
  and right spacer of equal flex weight, the leading items
@@ -47,6 +47,19 @@ export interface ActionBarItem {
47
47
  // a flex spacer so they pin to the right edge regardless of
48
48
  // registration order — system status indicators belong here.
49
49
  slot?: ActionBarItemSlot;
50
+ // Marks this item as admin-only. Hidden until the ActionBar's
51
+ // admin-mode toggle is switched on. The toggle button auto-appears
52
+ // on the bar whenever at least one registered item has adminOnly=
53
+ // true; when no consumer registers such items, the toggle stays
54
+ // hidden and the bar looks exactly as it always did.
55
+ //
56
+ // Use for elevated-privilege actions that would confuse regular
57
+ // users (version history, retry render, delete forever, etc.).
58
+ // Consumer decides eligibility upstream — typically by only
59
+ // registering the item when the caller has `capabilities.manage`
60
+ // (or an equivalent gate) === true. The kit does not check
61
+ // permissions; it only handles the toggle UX.
62
+ adminOnly?: boolean;
50
63
  }
51
64
 
52
65
  // Category definition — declared at the provider level. Drives