@svadmin/core 0.10.0 → 0.12.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": "@svadmin/core",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Core SDK — hooks, types, context, i18n, permissions, router",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -290,3 +290,61 @@ export function usePermissions<T = unknown>() {
290
290
  get error() { return error; },
291
291
  };
292
292
  }
293
+
294
+ // ─── useHasPermission ────────────────────────────────────────
295
+
296
+ /**
297
+ * Reactive permission checker — returns a closure that re-evaluates
298
+ * when the underlying permissions data changes.
299
+ *
300
+ * Works with Casbin-style permission arrays, Sets, or permission objects.
301
+ * The returned function is safe to use inside `$derived()` for reactive UI updates.
302
+ *
303
+ * @example
304
+ * ```svelte
305
+ * <script>
306
+ * import { useHasPermission } from '@svadmin/core';
307
+ *
308
+ * const hasPermission = useHasPermission();
309
+ * // ✅ Reactive — UI updates when permissions finish loading
310
+ * const canDelete = $derived(hasPermission('admin'));
311
+ * const canExport = $derived(hasPermission('data:export'));
312
+ * </script>
313
+ *
314
+ * {#if canDelete}
315
+ * <Button variant="destructive">Delete</Button>
316
+ * {/if}
317
+ * ```
318
+ */
319
+ export function useHasPermission() {
320
+ const provider = getAuthProvider();
321
+ let permissions = $state<unknown>(null);
322
+ let loaded = $state(false);
323
+
324
+ if (provider?.getPermissions) {
325
+ provider.getPermissions().then(p => {
326
+ permissions = p;
327
+ loaded = true;
328
+ }).catch(err => {
329
+ console.warn('[svadmin] useHasPermission: getPermissions failed', err);
330
+ loaded = true;
331
+ });
332
+ } else {
333
+ loaded = true;
334
+ }
335
+
336
+ /**
337
+ * Check if the current user has a specific permission.
338
+ * Supports: `string[]`, `Set<string>`, `Record<string, boolean>`, or any object with `.includes` / `.has`.
339
+ */
340
+ function hasPermission(perm: string): boolean {
341
+ if (!loaded || permissions == null) return false;
342
+ if (Array.isArray(permissions)) return permissions.includes(perm);
343
+ if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
344
+ if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
345
+ return false;
346
+ }
347
+
348
+ return hasPermission;
349
+ }
350
+
package/src/index.ts CHANGED
@@ -59,7 +59,7 @@ export type {
59
59
  DeleteParams, DeleteResult,
60
60
  DeleteManyParams, DeleteManyResult,
61
61
  Pagination, Sort, Filter, Identity,
62
- ResourceDefinition, FieldDefinition,
62
+ ResourceDefinition, FieldDefinition, MenuItem,
63
63
  AuthActionResult, CheckResult,
64
64
  ResourceTypeMap, KnownResources, InferData,
65
65
  BaseRecord,
@@ -79,7 +79,7 @@ export {
79
79
  useLogin, useLogout,
80
80
  useRegister, useForgotPassword, useUpdatePassword,
81
81
  useGetIdentity, useIsAuthenticated,
82
- useOnError, usePermissions,
82
+ useOnError, usePermissions, useHasPermission,
83
83
  } from './auth-hooks.svelte';
84
84
  export { useParsed } from './useParsed.svelte';
85
85
  export * from './useStepsForm.svelte';
package/src/types.ts CHANGED
@@ -260,6 +260,30 @@ export interface ResourceDefinition {
260
260
  meta?: Record<string, unknown> & { dataProviderName?: string; parent?: string };
261
261
  }
262
262
 
263
+ // ─── MenuItem (Multi-Level Menu) ──────────────────────────────
264
+
265
+ export interface MenuItem {
266
+ /** Unique identifier, also used as i18n fallback key: t(`menu.${name}`) */
267
+ name: string;
268
+ /** Display label. Falls back to `t('menu.${name}')` if omitted */
269
+ label?: string;
270
+ /** Icon name string (maps to lucide icon set) or Svelte component */
271
+ icon?: string;
272
+ /** Navigation path — omit for parent-only menu nodes */
273
+ href?: string;
274
+ /** Open in new tab (useful for external links) */
275
+ target?: '_blank' | '_self';
276
+ /** Permission check metadata */
277
+ meta?: {
278
+ resource?: string;
279
+ action?: string;
280
+ /** Hide this item from the menu entirely */
281
+ hidden?: boolean;
282
+ };
283
+ /** Sub-menu items — supports infinite nesting */
284
+ children?: MenuItem[];
285
+ }
286
+
263
287
  export interface FieldDefinition {
264
288
  key: string;
265
289
  label: string;