@cosxai/ui 0.10.2 → 0.10.4

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.2",
3
+ "version": "0.10.4",
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,24 +78,13 @@ 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
- }
81
+ // Admin mode is session-only — deliberately NOT persisted. Two
82
+ // reasons: (1) cross-page reload persistence would surface an
83
+ // elevated-privileges state on a fresh session without explicit
84
+ // user opt-in every time — a small surprise factor with real
85
+ // consequences on admin surfaces. (2) The reset-on-context-change
86
+ // effect below relies on the state living in React memory alone,
87
+ // so we can watch for admin-item-set changes and clear cleanly.
99
88
 
100
89
  interface BuildEntry {
101
90
  kind: "flat" | "group";
@@ -197,20 +186,48 @@ export function ActionBar({
197
186
  // ----- Admin mode toggle (any registered item with adminOnly=true) -----
198
187
  // Admin items stay hidden by default; a small toggle button (rendered
199
188
  // 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));
189
+ // Session-only: never persisted (see comment on loadAdminMode's
190
+ // removal above). Additionally auto-resets when the set of
191
+ // adminOnly item keys changes so a user who turned admin on for
192
+ // Doc A doesn't accidentally land in admin mode on Doc B.
193
+ const [adminMode, setAdminMode] = useState<boolean>(false);
204
194
  const toggleAdminMode = useCallback(() => {
205
- setAdminMode((prev) => {
206
- const next = !prev;
207
- persistAdminMode(storageKey, next);
208
- return next;
209
- });
210
- }, [storageKey]);
195
+ setAdminMode((prev) => !prev);
196
+ }, []);
197
+ const adminItemKeys = useMemo(
198
+ () =>
199
+ items
200
+ .filter((it) => it.adminOnly === true)
201
+ .map((it) => it.key)
202
+ .sort()
203
+ .join(","),
204
+ [items],
205
+ );
206
+ const prevAdminKeysRef = useRef(adminItemKeys);
207
+ useEffect(() => {
208
+ if (prevAdminKeysRef.current !== adminItemKeys) {
209
+ prevAdminKeysRef.current = adminItemKeys;
210
+ // Context changed (typically: user navigated to a different
211
+ // page whose useActionBarItems registered a different set of
212
+ // admin actions). Reset admin mode so the elevated state is
213
+ // never carried across viewer sessions implicitly.
214
+ setAdminMode(false);
215
+ }
216
+ }, [adminItemKeys]);
211
217
  const hasAdminItems = useMemo(() => items.some((it) => it.adminOnly === true), [items]);
218
+ // Filter semantics:
219
+ // admin OFF → hide items marked adminOnly (the toggle would reveal them)
220
+ // admin ON → hide items marked hiddenInAdmin (page opted them out of admin view)
221
+ // Together, adminOnly + hiddenInAdmin let pages choose:
222
+ // - additive: normal items unmarked → visible in both modes;
223
+ // admin items marked adminOnly → visible only in admin
224
+ // - exclusive: normal items marked hiddenInAdmin → hidden in admin;
225
+ // admin items marked adminOnly → visible only in admin
212
226
  const visibleItems = useMemo(
213
- () => (adminMode ? items : items.filter((it) => it.adminOnly !== true)),
227
+ () =>
228
+ adminMode
229
+ ? items.filter((it) => it.hiddenInAdmin !== true)
230
+ : items.filter((it) => it.adminOnly !== true),
214
231
  [items, adminMode],
215
232
  );
216
233
 
@@ -60,6 +60,18 @@ export interface ActionBarItem {
60
60
  // (or an equivalent gate) === true. The kit does not check
61
61
  // permissions; it only handles the toggle UX.
62
62
  adminOnly?: boolean;
63
+ // Marks a normally-visible item as hidden when admin mode is on.
64
+ // Symmetric counterpart to `adminOnly`: pages that want an
65
+ // exclusive "different toolset" UX (e.g. Share disappears in favour
66
+ // of Manage Share + Activity + History) mark their regular items
67
+ // hiddenInAdmin=true so they clear when the toggle flips on.
68
+ // Pages that want additive layering (regular items stay + admin
69
+ // items appear alongside) just leave this unset.
70
+ //
71
+ // Per-item because different pages within the same app want
72
+ // different behaviour: block_doc viewer wants exclusive; a
73
+ // hypothetical PDF admin surface might want additive.
74
+ hiddenInAdmin?: boolean;
63
75
  }
64
76
 
65
77
  // Category definition — declared at the provider level. Drives