@cosxai/ui 0.9.0 → 0.10.1

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.1",
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,63 @@ 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. 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. */}
468
+ {hasAdminItems && (
469
+ <button
470
+ type="button"
471
+ onClick={toggleAdminMode}
472
+ aria-label={adminMode ? "Exit admin mode" : "Enter admin mode"}
473
+ aria-pressed={adminMode}
474
+ title={adminMode ? "Admin mode · click to exit" : "Enter admin mode"}
475
+ data-ck-actionbar-admin-toggle
476
+ data-active={adminMode ? "1" : "0"}
477
+ style={{
478
+ display: "inline-flex",
479
+ alignItems: "center",
480
+ justifyContent: "center",
481
+ width: 28,
482
+ height: 28,
483
+ padding: 0,
484
+ marginLeft: 2,
485
+ marginRight: 2,
486
+ borderRadius: 999,
487
+ background: "transparent",
488
+ color: adminMode
489
+ ? "var(--ck-accent, #4f46e5)"
490
+ : "var(--ck-text-secondary, #666)",
491
+ border: "none",
492
+ cursor: "pointer",
493
+ transition: "color 160ms ease-out",
494
+ }}
495
+ >
496
+ <svg
497
+ width="16"
498
+ height="16"
499
+ viewBox="0 0 24 24"
500
+ fill="none"
501
+ stroke="currentColor"
502
+ strokeWidth="1.8"
503
+ strokeLinecap="round"
504
+ strokeLinejoin="round"
505
+ aria-hidden
506
+ >
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" />
515
+ </svg>
516
+ </button>
517
+ )}
518
+
414
519
  {/* Balancing leading spacer — only present when the right side
415
520
  holds ONLY a status dot (no trailing items). With a left
416
521
  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