preact-missing-hooks 4.4.0 → 4.6.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/dist/react.js CHANGED
@@ -1776,6 +1776,314 @@ function useRefPrint(printRef, options = {}) {
1776
1776
  return { print };
1777
1777
  }
1778
1778
 
1779
+ const WILDCARD = "*";
1780
+ function parseUserFromStorage(type, key) {
1781
+ if (typeof window === "undefined")
1782
+ return null;
1783
+ const storage = type === "localStorage" ? window.localStorage : window.sessionStorage;
1784
+ try {
1785
+ const raw = storage.getItem(key);
1786
+ if (raw == null)
1787
+ return null;
1788
+ const data = JSON.parse(raw);
1789
+ return data && typeof data === "object" ? data : null;
1790
+ }
1791
+ catch (_a) {
1792
+ return null;
1793
+ }
1794
+ }
1795
+ function parseCapabilitiesFromStorage(type, key) {
1796
+ if (typeof window === "undefined")
1797
+ return [];
1798
+ const storage = type === "localStorage" ? window.localStorage : window.sessionStorage;
1799
+ try {
1800
+ const raw = storage.getItem(key);
1801
+ if (raw == null)
1802
+ return [];
1803
+ const data = JSON.parse(raw);
1804
+ return Array.isArray(data)
1805
+ ? data.filter((x) => typeof x === "string")
1806
+ : [];
1807
+ }
1808
+ catch (_a) {
1809
+ return [];
1810
+ }
1811
+ }
1812
+ function computeRoles(user, roleDefinitions) {
1813
+ return roleDefinitions
1814
+ .filter((def) => def.condition(user))
1815
+ .map((def) => def.role);
1816
+ }
1817
+ function computeCapabilitiesFromRoles(roles, roleCapabilities) {
1818
+ const set = new Set();
1819
+ for (const role of roles) {
1820
+ const caps = roleCapabilities[role];
1821
+ if (Array.isArray(caps)) {
1822
+ for (const c of caps)
1823
+ set.add(c);
1824
+ }
1825
+ }
1826
+ return Array.from(set);
1827
+ }
1828
+ function hasCapabilityImpl(capabilities, capability) {
1829
+ if (capabilities.includes(WILDCARD))
1830
+ return true;
1831
+ return capabilities.includes(capability);
1832
+ }
1833
+ /**
1834
+ * Frontend-only role-based access control hook. Define roles with conditions,
1835
+ * assign capabilities per role, and plug in user source (localStorage, sessionStorage, API, or custom).
1836
+ * Supports full flexibility: frontend-only with storage or pluggable API.
1837
+ *
1838
+ * @param options - userSource, roleDefinitions, roleCapabilities, optional capabilitiesOverride
1839
+ * @returns user, roles, capabilities, hasRole, hasCapability, can, isReady, error, refetch, and storage helpers
1840
+ *
1841
+ * @example
1842
+ * ```tsx
1843
+ * const { hasRole, can, roles, setUserInStorage } = useRBAC({
1844
+ * userSource: { type: 'localStorage', key: 'user' },
1845
+ * roleDefinitions: [
1846
+ * { role: 'admin', condition: (u) => u?.role === 'admin' },
1847
+ * { role: 'editor', condition: (u) => u?.role === 'editor' || u?.role === 'admin' },
1848
+ * ],
1849
+ * roleCapabilities: {
1850
+ * admin: ['*'],
1851
+ * editor: ['posts:edit', 'posts:create'],
1852
+ * },
1853
+ * });
1854
+ * if (can('posts:edit')) { ... }
1855
+ * ```
1856
+ */
1857
+ function useRBAC(options) {
1858
+ const { userSource } = options;
1859
+ const optsRef = react.useRef(options);
1860
+ optsRef.current = options;
1861
+ const [user, setUser] = react.useState(null);
1862
+ const [roles, setRoles] = react.useState([]);
1863
+ const [capabilities, setCapabilities] = react.useState([]);
1864
+ const [isReady, setIsReady] = react.useState(false);
1865
+ const [error, setError] = react.useState(null);
1866
+ const resolveAuth = react.useCallback(() => __awaiter(this, void 0, void 0, function* () {
1867
+ var _a, _b, _c;
1868
+ const { userSource: us, roleDefinitions: rdefs, roleCapabilities: rcaps, capabilitiesOverride: capOver, } = optsRef.current;
1869
+ setError(null);
1870
+ let resolvedUser = null;
1871
+ let resolvedRoles = [];
1872
+ let resolvedCapabilities = [];
1873
+ try {
1874
+ if (us.type === "localStorage") {
1875
+ resolvedUser = parseUserFromStorage("localStorage", us.key);
1876
+ resolvedRoles = computeRoles(resolvedUser, rdefs);
1877
+ resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
1878
+ }
1879
+ else if (us.type === "sessionStorage") {
1880
+ resolvedUser = parseUserFromStorage("sessionStorage", us.key);
1881
+ resolvedRoles = computeRoles(resolvedUser, rdefs);
1882
+ resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
1883
+ }
1884
+ else if (us.type === "api") {
1885
+ resolvedUser = yield us.fetch();
1886
+ resolvedRoles = computeRoles(resolvedUser, rdefs);
1887
+ resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
1888
+ }
1889
+ else if (us.type === "memory") {
1890
+ resolvedUser = us.getUser();
1891
+ resolvedRoles = computeRoles(resolvedUser, rdefs);
1892
+ resolvedCapabilities = computeCapabilitiesFromRoles(resolvedRoles, rcaps);
1893
+ }
1894
+ else if (us.type === "custom") {
1895
+ const auth = yield Promise.resolve(us.getAuth());
1896
+ resolvedUser = (_a = auth.user) !== null && _a !== void 0 ? _a : null;
1897
+ resolvedRoles = (_b = auth.roles) !== null && _b !== void 0 ? _b : computeRoles(resolvedUser, rdefs);
1898
+ resolvedCapabilities =
1899
+ (_c = auth.capabilities) !== null && _c !== void 0 ? _c : computeCapabilitiesFromRoles(resolvedRoles, rcaps);
1900
+ }
1901
+ if (capOver) {
1902
+ if (capOver.type === "localStorage") {
1903
+ const override = parseCapabilitiesFromStorage("localStorage", capOver.key);
1904
+ if (override.length > 0)
1905
+ resolvedCapabilities = override;
1906
+ }
1907
+ else if (capOver.type === "sessionStorage") {
1908
+ const override = parseCapabilitiesFromStorage("sessionStorage", capOver.key);
1909
+ if (override.length > 0)
1910
+ resolvedCapabilities = override;
1911
+ }
1912
+ else if (capOver.type === "api") {
1913
+ const override = yield capOver.fetch();
1914
+ resolvedCapabilities = override;
1915
+ }
1916
+ }
1917
+ setUser(resolvedUser);
1918
+ setRoles(resolvedRoles);
1919
+ setCapabilities(resolvedCapabilities);
1920
+ }
1921
+ catch (e) {
1922
+ setError(e instanceof Error ? e : new Error(String(e)));
1923
+ setUser(null);
1924
+ setRoles([]);
1925
+ setCapabilities([]);
1926
+ }
1927
+ finally {
1928
+ setIsReady(true);
1929
+ }
1930
+ }), []);
1931
+ react.useEffect(() => {
1932
+ resolveAuth();
1933
+ }, [resolveAuth]);
1934
+ // Listen to storage events when using localStorage/sessionStorage so we refetch when another tab changes data
1935
+ react.useEffect(() => {
1936
+ if (typeof window === "undefined")
1937
+ return;
1938
+ const key = userSource.type === "localStorage" || userSource.type === "sessionStorage"
1939
+ ? userSource.key
1940
+ : null;
1941
+ if (!key)
1942
+ return;
1943
+ const handler = (e) => {
1944
+ if (e.key === key)
1945
+ void resolveAuth();
1946
+ };
1947
+ window.addEventListener("storage", handler);
1948
+ return () => window.removeEventListener("storage", handler);
1949
+ }, [
1950
+ userSource.type,
1951
+ userSource.type === "localStorage" || userSource.type === "sessionStorage"
1952
+ ? userSource.key
1953
+ : "",
1954
+ resolveAuth,
1955
+ ]);
1956
+ const hasRole = react.useCallback((role) => roles.includes(role), [roles]);
1957
+ const hasCapability = react.useCallback((capability) => hasCapabilityImpl(capabilities, capability), [capabilities]);
1958
+ const can = hasCapability;
1959
+ const refetch = react.useCallback(() => resolveAuth(), [resolveAuth]);
1960
+ const setUserInStorage = react.useCallback((newUser, storage, key) => {
1961
+ if (typeof window === "undefined")
1962
+ return;
1963
+ const s = storage === "localStorage"
1964
+ ? window.localStorage
1965
+ : window.sessionStorage;
1966
+ if (newUser == null)
1967
+ s.removeItem(key);
1968
+ else
1969
+ s.setItem(key, JSON.stringify(newUser));
1970
+ if ((userSource.type === "localStorage" &&
1971
+ storage === "localStorage" &&
1972
+ userSource.key === key) ||
1973
+ (userSource.type === "sessionStorage" &&
1974
+ storage === "sessionStorage" &&
1975
+ userSource.key === key)) {
1976
+ void resolveAuth();
1977
+ }
1978
+ }, [userSource, resolveAuth]);
1979
+ const setRolesInStorage = react.useCallback((newRoles, storage, key) => {
1980
+ if (typeof window === "undefined")
1981
+ return;
1982
+ const s = storage === "localStorage"
1983
+ ? window.localStorage
1984
+ : window.sessionStorage;
1985
+ s.setItem(key, JSON.stringify(newRoles));
1986
+ }, []);
1987
+ const setCapabilitiesInStorage = react.useCallback((newCaps, storage, key) => {
1988
+ if (typeof window === "undefined")
1989
+ return;
1990
+ const s = storage === "localStorage"
1991
+ ? window.localStorage
1992
+ : window.sessionStorage;
1993
+ s.setItem(key, JSON.stringify(newCaps));
1994
+ }, []);
1995
+ return {
1996
+ user,
1997
+ roles,
1998
+ capabilities,
1999
+ isReady,
2000
+ error,
2001
+ hasRole,
2002
+ hasCapability,
2003
+ can,
2004
+ refetch,
2005
+ setUserInStorage,
2006
+ setRolesInStorage,
2007
+ setCapabilitiesInStorage,
2008
+ };
2009
+ }
2010
+
2011
+ function prefetchDocument(url) {
2012
+ if (typeof document === "undefined")
2013
+ return;
2014
+ const links = document.querySelectorAll('link[rel="prefetch"]');
2015
+ for (let i = 0; i < links.length; i++) {
2016
+ if (links[i].href === url)
2017
+ return;
2018
+ }
2019
+ const link = document.createElement("link");
2020
+ link.rel = "prefetch";
2021
+ link.href = url;
2022
+ document.head.appendChild(link);
2023
+ }
2024
+ function prefetchFetch(url) {
2025
+ if (typeof fetch === "undefined")
2026
+ return;
2027
+ fetch(url, { method: "GET", mode: "cors" }).catch(() => {
2028
+ /* ignore; prefetch is best-effort */
2029
+ });
2030
+ }
2031
+ /**
2032
+ * A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
2033
+ * so they are cached before the user navigates or needs them. Useful for link hover or
2034
+ * route preloading.
2035
+ *
2036
+ * @returns Object with prefetch(url, options?) and isPrefetched(url)
2037
+ *
2038
+ * @example
2039
+ * ```tsx
2040
+ * function NavLink({ href, children }) {
2041
+ * const { prefetch } = usePrefetch();
2042
+ * return (
2043
+ * <a
2044
+ * href={href}
2045
+ * onMouseEnter={() => prefetch(href)}
2046
+ * >
2047
+ * {children}
2048
+ * </a>
2049
+ * );
2050
+ * }
2051
+ * ```
2052
+ *
2053
+ * @example
2054
+ * ```tsx
2055
+ * // Prefetch API data
2056
+ * const { prefetch } = usePrefetch();
2057
+ * prefetch('/api/user', { as: 'fetch' });
2058
+ * ```
2059
+ */
2060
+ function usePrefetch() {
2061
+ const prefetchedRef = react.useRef(new Set());
2062
+ const [, setTick] = react.useState(0);
2063
+ const prefetch = react.useCallback((url, options) => {
2064
+ var _a;
2065
+ const trimmed = url === null || url === void 0 ? void 0 : url.trim();
2066
+ if (!trimmed)
2067
+ return;
2068
+ if (prefetchedRef.current.has(trimmed))
2069
+ return;
2070
+ const as = (_a = options === null || options === void 0 ? void 0 : options.as) !== null && _a !== void 0 ? _a : "document";
2071
+ if (as === "document") {
2072
+ prefetchDocument(trimmed);
2073
+ }
2074
+ else {
2075
+ prefetchFetch(trimmed);
2076
+ }
2077
+ prefetchedRef.current.add(trimmed);
2078
+ setTick((n) => n + 1);
2079
+ }, []);
2080
+ const isPrefetched = react.useCallback((url) => {
2081
+ var _a;
2082
+ return prefetchedRef.current.has((_a = url === null || url === void 0 ? void 0 : url.trim()) !== null && _a !== void 0 ? _a : "");
2083
+ }, []);
2084
+ return { prefetch, isPrefetched };
2085
+ }
2086
+
1779
2087
  exports.useClipboard = useClipboard;
1780
2088
  exports.useEventBus = useEventBus;
1781
2089
  exports.useIndexedDB = useIndexedDB;
@@ -1783,6 +2091,8 @@ exports.useLLMMetadata = useLLMMetadata;
1783
2091
  exports.useMutationObserver = useMutationObserver;
1784
2092
  exports.useNetworkState = useNetworkState;
1785
2093
  exports.usePreferredTheme = usePreferredTheme;
2094
+ exports.usePrefetch = usePrefetch;
2095
+ exports.useRBAC = useRBAC;
1786
2096
  exports.useRageClick = useRageClick;
1787
2097
  exports.useRefPrint = useRefPrint;
1788
2098
  exports.useThreadedWorker = useThreadedWorker;
@@ -0,0 +1,44 @@
1
+ export interface UsePrefetchOptions {
2
+ /**
3
+ * Resource type for prefetch.
4
+ * - "document": uses <link rel="prefetch"> (default, for next navigation)
5
+ * - "fetch": uses fetch() to warm the HTTP cache (e.g. for API or same-origin data)
6
+ */
7
+ as?: "document" | "fetch";
8
+ }
9
+ export interface UsePrefetchReturn {
10
+ /** Prefetch a URL so it is cached for later use. No-op if URL was already prefetched or empty. */
11
+ prefetch: (url: string, options?: UsePrefetchOptions) => void;
12
+ /** Check whether a URL has already been prefetched in this hook instance. */
13
+ isPrefetched: (url: string) => boolean;
14
+ }
15
+ /**
16
+ * A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
17
+ * so they are cached before the user navigates or needs them. Useful for link hover or
18
+ * route preloading.
19
+ *
20
+ * @returns Object with prefetch(url, options?) and isPrefetched(url)
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * function NavLink({ href, children }) {
25
+ * const { prefetch } = usePrefetch();
26
+ * return (
27
+ * <a
28
+ * href={href}
29
+ * onMouseEnter={() => prefetch(href)}
30
+ * >
31
+ * {children}
32
+ * </a>
33
+ * );
34
+ * }
35
+ * ```
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * // Prefetch API data
40
+ * const { prefetch } = usePrefetch();
41
+ * prefetch('/api/user', { as: 'fetch' });
42
+ * ```
43
+ */
44
+ export declare function usePrefetch(): UsePrefetchReturn;
@@ -0,0 +1,102 @@
1
+ /** Flexible user object; your app can use any shape. */
2
+ export type RBACUser = Record<string, unknown>;
3
+ /** Get auth state (user + optional roles/capabilities). Used by custom source. */
4
+ export interface RBACAuthState {
5
+ user?: RBACUser | null;
6
+ roles?: string[];
7
+ capabilities?: string[];
8
+ }
9
+ /** Pluggable source for current user (and optionally roles/capabilities). */
10
+ export type RBACUserSource = {
11
+ type: "localStorage";
12
+ key: string;
13
+ } | {
14
+ type: "sessionStorage";
15
+ key: string;
16
+ } | {
17
+ type: "api";
18
+ fetch: () => Promise<RBACUser>;
19
+ } | {
20
+ type: "memory";
21
+ getUser: () => RBACUser | null;
22
+ } | {
23
+ type: "custom";
24
+ getAuth: () => RBACAuthState | Promise<RBACAuthState>;
25
+ };
26
+ /** Role definition: name + condition to grant this role based on user. */
27
+ export interface RBACRoleDefinition {
28
+ role: string;
29
+ condition: (user: RBACUser | null) => boolean;
30
+ }
31
+ /** Role name -> list of capability strings. Use '*' for full access. */
32
+ export type RBACRoleCapabilities = Record<string, string[]>;
33
+ /** Optional override: get capabilities directly (e.g. from API) instead of deriving from roles. */
34
+ export type RBACCapabilitiesOverride = {
35
+ type: "localStorage";
36
+ key: string;
37
+ } | {
38
+ type: "sessionStorage";
39
+ key: string;
40
+ } | {
41
+ type: "api";
42
+ fetch: () => Promise<string[]>;
43
+ };
44
+ export interface UseRBACOptions {
45
+ /** Where to get the current user (and optionally roles/capabilities if type is 'custom'). */
46
+ userSource: RBACUserSource;
47
+ /** Role definitions: each role has a condition(user) to determine if the user has that role. */
48
+ roleDefinitions: RBACRoleDefinition[];
49
+ /** Capabilities per role. User gets union of capabilities for all their roles. Use '*' for admin. */
50
+ roleCapabilities: RBACRoleCapabilities;
51
+ /** Optional: fetch capabilities directly (overrides role-derived capabilities when provided). */
52
+ capabilitiesOverride?: RBACCapabilitiesOverride;
53
+ }
54
+ export interface UseRBACReturn {
55
+ /** Current user from source, or null. */
56
+ user: RBACUser | null;
57
+ /** Resolved roles for the current user. */
58
+ roles: string[];
59
+ /** Resolved capabilities (union of role capabilities, or from override). */
60
+ capabilities: string[];
61
+ /** True when user/roles/capabilities have been resolved (or failed). */
62
+ isReady: boolean;
63
+ /** Error from source (e.g. API or parse). */
64
+ error: Error | null;
65
+ /** Check if the user has the given role. */
66
+ hasRole: (role: string) => boolean;
67
+ /** Check if the user has the given capability (or '*' ). */
68
+ hasCapability: (capability: string) => boolean;
69
+ /** Alias for hasCapability. */
70
+ can: (capability: string) => boolean;
71
+ /** Re-fetch user/roles/capabilities from source. */
72
+ refetch: () => Promise<void>;
73
+ /** Helpers to persist auth to storage (for frontend-only flows). */
74
+ setUserInStorage: (user: RBACUser | null, storage: "localStorage" | "sessionStorage", key: string) => void;
75
+ setRolesInStorage: (roles: string[], storage: "localStorage" | "sessionStorage", key: string) => void;
76
+ setCapabilitiesInStorage: (capabilities: string[], storage: "localStorage" | "sessionStorage", key: string) => void;
77
+ }
78
+ /**
79
+ * Frontend-only role-based access control hook. Define roles with conditions,
80
+ * assign capabilities per role, and plug in user source (localStorage, sessionStorage, API, or custom).
81
+ * Supports full flexibility: frontend-only with storage or pluggable API.
82
+ *
83
+ * @param options - userSource, roleDefinitions, roleCapabilities, optional capabilitiesOverride
84
+ * @returns user, roles, capabilities, hasRole, hasCapability, can, isReady, error, refetch, and storage helpers
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * const { hasRole, can, roles, setUserInStorage } = useRBAC({
89
+ * userSource: { type: 'localStorage', key: 'user' },
90
+ * roleDefinitions: [
91
+ * { role: 'admin', condition: (u) => u?.role === 'admin' },
92
+ * { role: 'editor', condition: (u) => u?.role === 'editor' || u?.role === 'admin' },
93
+ * ],
94
+ * roleCapabilities: {
95
+ * admin: ['*'],
96
+ * editor: ['posts:edit', 'posts:create'],
97
+ * },
98
+ * });
99
+ * if (can('posts:edit')) { ... }
100
+ * ```
101
+ */
102
+ export declare function useRBAC(options: UseRBACOptions): UseRBACReturn;
package/docs/README.md CHANGED
@@ -41,6 +41,7 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
41
41
  | **useWrappedChildren** | Children buttons get injected styles. |
42
42
  | **usePreferredTheme** | Shows light / dark / no-preference from system. |
43
43
  | **useNetworkState** | Online/offline and connection type. |
44
+ | **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
44
45
  | **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
45
46
  | **useRageClick** | Click the area 3+ times quickly; rage click count. |
46
47
  | **useThreadedWorker** | Run a task; see loading and result. |
@@ -48,6 +49,8 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
48
49
  | **useWebRTCIP** | Detect IP via WebRTC (may take a few seconds). |
49
50
  | **useWasmCompute** | Run WASM in a worker (needs `add.wasm` in docs). |
50
51
  | **useWorkerNotifications** | Run/fail tasks and queue updates; toasts show events. |
52
+ | **useRefPrint** | Bind a ref to a section and click “Print / Save as PDF”; only that section is printed via `@media print`. |
53
+ | **useRBAC** | Login as Admin / Editor / Viewer (localStorage or sessionStorage); see roles and capabilities; conditional UI by `can(...)`. |
51
54
  | **useLLMMetadata** | Change “route” with buttons; see injected script info in the Live panel and `<script data-llm="true">` in the document head. Safe with `null`/`undefined` config (minimal payload with `route: "/"`). |
52
55
 
53
56
  ---
@@ -88,6 +91,110 @@ function App() {
88
91
  }
89
92
  ```
90
93
 
94
+ ### useRefPrint example
95
+
96
+ Print only a specific section via the native print dialog (user can save as PDF):
97
+
98
+ ```js
99
+ import { h, render } from "preact";
100
+ import { useRef } from "preact/hooks";
101
+ import { useRefPrint } from "preact-missing-hooks";
102
+
103
+ function Report() {
104
+ const printRef = useRef(null);
105
+ const { print } = useRefPrint(printRef, {
106
+ documentTitle: "My Report",
107
+ downloadAsPdf: true,
108
+ });
109
+ return h(
110
+ "div",
111
+ {},
112
+ h(
113
+ "div",
114
+ { ref: printRef, style: { padding: "1rem", background: "#f5f5f5" } },
115
+ "Only this section is printed when you click the button."
116
+ ),
117
+ h("button", { onClick: print }, "Print / Save as PDF")
118
+ );
119
+ }
120
+ render(h(Report), document.getElementById("root"));
121
+ ```
122
+
123
+ ### useRBAC example
124
+
125
+ Frontend-only role-based access: define roles with conditions, assign capabilities per role, and read the current user from localStorage (or sessionStorage / API):
126
+
127
+ ```js
128
+ import { h, render } from "preact";
129
+ import { useRBAC } from "preact-missing-hooks";
130
+
131
+ const roleDefinitions = [
132
+ { role: "admin", condition: (u) => u && u.role === "admin" },
133
+ {
134
+ role: "editor",
135
+ condition: (u) => u && (u.role === "editor" || u.role === "admin"),
136
+ },
137
+ { role: "viewer", condition: (u) => u && u.id != null },
138
+ ];
139
+ const roleCapabilities = {
140
+ admin: ["*"],
141
+ editor: ["posts:edit", "posts:create", "posts:read"],
142
+ viewer: ["posts:read"],
143
+ };
144
+
145
+ function App() {
146
+ const { user, roles, can, setUserInStorage } = useRBAC({
147
+ userSource: { type: "localStorage", key: "app-user" },
148
+ roleDefinitions,
149
+ roleCapabilities,
150
+ });
151
+
152
+ if (!user) {
153
+ return h(
154
+ "div",
155
+ {},
156
+ h(
157
+ "button",
158
+ {
159
+ onClick: () =>
160
+ setUserInStorage(
161
+ { id: 1, role: "admin" },
162
+ "localStorage",
163
+ "app-user"
164
+ ),
165
+ },
166
+ "Login as Admin"
167
+ ),
168
+ h(
169
+ "button",
170
+ {
171
+ onClick: () =>
172
+ setUserInStorage(
173
+ { id: 2, role: "viewer" },
174
+ "localStorage",
175
+ "app-user"
176
+ ),
177
+ },
178
+ "Login as Viewer"
179
+ )
180
+ );
181
+ }
182
+ return h(
183
+ "div",
184
+ {},
185
+ h("p", {}, "Roles: " + roles.join(", ")),
186
+ can("posts:edit") && h("button", {}, "Edit post"),
187
+ can("*") && h("button", {}, "Admin panel"),
188
+ h(
189
+ "button",
190
+ { onClick: () => setUserInStorage(null, "localStorage", "app-user") },
191
+ "Logout"
192
+ )
193
+ );
194
+ }
195
+ render(h(App), document.getElementById("root"));
196
+ ```
197
+
91
198
  ### useLLMMetadata in the demo
92
199
 
93
200
  The **useLLMMetadata** Live panel simulates route changes. Click “Route: /”, “Route: /blog”, or “Route: /docs”. Each change:
package/docs/index.html CHANGED
@@ -323,6 +323,56 @@
323
323
  transform: translateX(0);
324
324
  }
325
325
  }
326
+ /* useRBAC demo */
327
+ .rbac-demo {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 0.75rem;
331
+ }
332
+ .rbac-toolbar {
333
+ display: flex;
334
+ align-items: center;
335
+ flex-wrap: wrap;
336
+ gap: 0.25rem;
337
+ }
338
+ .rbac-toolbar button {
339
+ padding: 0.25rem 0.5rem;
340
+ font-size: 0.8rem;
341
+ cursor: pointer;
342
+ background: var(--surface2);
343
+ border: 1px solid var(--border);
344
+ border-radius: var(--radiusSm);
345
+ color: var(--text);
346
+ }
347
+ .rbac-toolbar button:hover {
348
+ background: var(--border);
349
+ }
350
+ .rbac-actions {
351
+ display: flex;
352
+ flex-wrap: wrap;
353
+ gap: 0.35rem;
354
+ }
355
+ .rbac-state {
356
+ background: var(--surface2);
357
+ border-radius: var(--radiusSm);
358
+ padding: 0.6rem 0.75rem;
359
+ font-size: 0.85rem;
360
+ }
361
+ .rbac-state-row {
362
+ margin-bottom: 0.35rem;
363
+ }
364
+ .rbac-state-row:last-child {
365
+ margin-bottom: 0;
366
+ }
367
+ .rbac-state-row code {
368
+ margin-left: 0.25rem;
369
+ }
370
+ .rbac-conditional {
371
+ display: flex;
372
+ flex-wrap: wrap;
373
+ align-items: center;
374
+ gap: 0.25rem;
375
+ }
326
376
  </style>
327
377
  </head>
328
378
  <body>