@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 +1 -1
- package/src/actionbar/ActionBar.tsx +109 -4
- package/src/actionbar/types.ts +13 -0
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
}, [
|
|
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
|
-
|
|
363
|
-
|
|
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
|
package/src/actionbar/types.ts
CHANGED
|
@@ -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
|