@classic-homes/theme-svelte 0.1.55 → 1.0.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/__tests__/nav-access.test.js +145 -0
- package/dist/__tests__/sidebar-expansion.test.js +59 -0
- package/dist/lib/components/Button.svelte +17 -1
- package/dist/lib/components/Checkbox.svelte +3 -2
- package/dist/lib/components/Combobox.svelte +4 -4
- package/dist/lib/components/DataPanel.svelte +11 -5
- package/dist/lib/components/DateTimePicker.svelte +3 -2
- package/dist/lib/components/FileUpload.svelte +4 -3
- package/dist/lib/components/HeaderSearch.svelte +5 -0
- package/dist/lib/components/RadioGroup.svelte +3 -2
- package/dist/lib/components/Select.svelte +3 -2
- package/dist/lib/components/Signature.svelte +3 -2
- package/dist/lib/components/Tabs.svelte +3 -2
- package/dist/lib/components/data-table/DataTableColumnFilter.svelte +2 -2
- package/dist/lib/components/data-table/DataTableColumnFilter.svelte.d.ts +33 -13
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +2 -2
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte.d.ts +27 -7
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte +2 -2
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte.d.ts +36 -17
- package/dist/lib/components/data-table/DataTablePagination.svelte +2 -2
- package/dist/lib/components/data-table/DataTablePagination.svelte.d.ts +31 -11
- package/dist/lib/components/data-table/DataTableSearch.svelte +2 -2
- package/dist/lib/components/data-table/DataTableSearch.svelte.d.ts +31 -11
- package/dist/lib/components/data-table/DataTableToolbar.svelte +2 -2
- package/dist/lib/components/data-table/DataTableToolbar.svelte.d.ts +37 -17
- package/dist/lib/components/layout/DashboardLayout.svelte +1 -0
- package/dist/lib/components/layout/Sidebar.svelte +22 -17
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +10 -1
- package/dist/lib/composables/useAnnounce.svelte.js +9 -9
- package/dist/lib/composables/useArrowNavigation.svelte.js +5 -4
- package/dist/lib/composables/useEscapeKey.svelte.d.ts +4 -2
- package/dist/lib/composables/useEscapeKey.svelte.js +3 -2
- package/dist/lib/composables/useFocusRef.svelte.js +4 -3
- package/dist/lib/composables/useFocusTrap.svelte.d.ts +9 -3
- package/dist/lib/composables/useFocusTrap.svelte.js +34 -28
- package/dist/lib/composables/useRovingTabindex.svelte.js +4 -3
- package/dist/lib/schemas/auth.d.ts +18 -8
- package/dist/lib/schemas/auth.js +4 -1
- package/dist/lib/schemas/index.d.ts +1 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +7 -2
- package/dist/lib/stores/sidebar.svelte.js +9 -3
- package/dist/lib/types/layout.d.ts +52 -4
- package/dist/lib/utils/nav-access.d.ts +61 -0
- package/dist/lib/utils/nav-access.js +74 -0
- package/dist/lib/utils.d.ts +0 -10
- package/dist/lib/utils.js +0 -13
- package/package.json +1 -1
|
@@ -243,7 +243,10 @@ export declare const emailChangeSchema: z.ZodObject<{
|
|
|
243
243
|
}>;
|
|
244
244
|
export type EmailChangeFormData = z.infer<typeof emailChangeSchema>;
|
|
245
245
|
/**
|
|
246
|
-
* User schema
|
|
246
|
+
* User schema — for validating a user payload from an API.
|
|
247
|
+
*
|
|
248
|
+
* Distinct from the layout `User` in `types/layout.ts`, which is a display
|
|
249
|
+
* view-model for the sidebar and avatar. This one describes data on the wire.
|
|
247
250
|
*/
|
|
248
251
|
export declare const userSchema: z.ZodObject<{
|
|
249
252
|
id: z.ZodString;
|
|
@@ -262,9 +265,9 @@ export declare const userSchema: z.ZodObject<{
|
|
|
262
265
|
email: string;
|
|
263
266
|
username?: string | undefined;
|
|
264
267
|
roles?: string[] | undefined;
|
|
268
|
+
permissions?: string[] | undefined;
|
|
265
269
|
fullName?: string | undefined;
|
|
266
270
|
avatarUrl?: string | null | undefined;
|
|
267
|
-
permissions?: string[] | undefined;
|
|
268
271
|
mfaEnabled?: boolean | undefined;
|
|
269
272
|
emailVerified?: boolean | undefined;
|
|
270
273
|
createdAt?: string | undefined;
|
|
@@ -274,15 +277,22 @@ export declare const userSchema: z.ZodObject<{
|
|
|
274
277
|
email: string;
|
|
275
278
|
username?: string | undefined;
|
|
276
279
|
roles?: string[] | undefined;
|
|
280
|
+
permissions?: string[] | undefined;
|
|
277
281
|
fullName?: string | undefined;
|
|
278
282
|
avatarUrl?: string | null | undefined;
|
|
279
|
-
permissions?: string[] | undefined;
|
|
280
283
|
mfaEnabled?: boolean | undefined;
|
|
281
284
|
emailVerified?: boolean | undefined;
|
|
282
285
|
createdAt?: string | undefined;
|
|
283
286
|
updatedAt?: string | undefined;
|
|
284
287
|
}>;
|
|
285
|
-
|
|
288
|
+
/**
|
|
289
|
+
* A user payload as validated by {@link userSchema}.
|
|
290
|
+
*
|
|
291
|
+
* Named `AuthUser` to keep the package's `User` unambiguous — that name belongs
|
|
292
|
+
* to the layout view-model. Previously exported as `User`, where it collided
|
|
293
|
+
* with the layout type and lost, making it unreachable from the package root.
|
|
294
|
+
*/
|
|
295
|
+
export type AuthUser = z.infer<typeof userSchema>;
|
|
286
296
|
/**
|
|
287
297
|
* Auth tokens schema
|
|
288
298
|
*/
|
|
@@ -324,9 +334,9 @@ export declare const loginResponseSchema: z.ZodObject<{
|
|
|
324
334
|
email: string;
|
|
325
335
|
username?: string | undefined;
|
|
326
336
|
roles?: string[] | undefined;
|
|
337
|
+
permissions?: string[] | undefined;
|
|
327
338
|
fullName?: string | undefined;
|
|
328
339
|
avatarUrl?: string | null | undefined;
|
|
329
|
-
permissions?: string[] | undefined;
|
|
330
340
|
mfaEnabled?: boolean | undefined;
|
|
331
341
|
emailVerified?: boolean | undefined;
|
|
332
342
|
createdAt?: string | undefined;
|
|
@@ -336,9 +346,9 @@ export declare const loginResponseSchema: z.ZodObject<{
|
|
|
336
346
|
email: string;
|
|
337
347
|
username?: string | undefined;
|
|
338
348
|
roles?: string[] | undefined;
|
|
349
|
+
permissions?: string[] | undefined;
|
|
339
350
|
fullName?: string | undefined;
|
|
340
351
|
avatarUrl?: string | null | undefined;
|
|
341
|
-
permissions?: string[] | undefined;
|
|
342
352
|
mfaEnabled?: boolean | undefined;
|
|
343
353
|
emailVerified?: boolean | undefined;
|
|
344
354
|
createdAt?: string | undefined;
|
|
@@ -368,9 +378,9 @@ export declare const loginResponseSchema: z.ZodObject<{
|
|
|
368
378
|
email: string;
|
|
369
379
|
username?: string | undefined;
|
|
370
380
|
roles?: string[] | undefined;
|
|
381
|
+
permissions?: string[] | undefined;
|
|
371
382
|
fullName?: string | undefined;
|
|
372
383
|
avatarUrl?: string | null | undefined;
|
|
373
|
-
permissions?: string[] | undefined;
|
|
374
384
|
mfaEnabled?: boolean | undefined;
|
|
375
385
|
emailVerified?: boolean | undefined;
|
|
376
386
|
createdAt?: string | undefined;
|
|
@@ -390,9 +400,9 @@ export declare const loginResponseSchema: z.ZodObject<{
|
|
|
390
400
|
email: string;
|
|
391
401
|
username?: string | undefined;
|
|
392
402
|
roles?: string[] | undefined;
|
|
403
|
+
permissions?: string[] | undefined;
|
|
393
404
|
fullName?: string | undefined;
|
|
394
405
|
avatarUrl?: string | null | undefined;
|
|
395
|
-
permissions?: string[] | undefined;
|
|
396
406
|
mfaEnabled?: boolean | undefined;
|
|
397
407
|
emailVerified?: boolean | undefined;
|
|
398
408
|
createdAt?: string | undefined;
|
package/dist/lib/schemas/auth.js
CHANGED
|
@@ -181,7 +181,10 @@ export const emailChangeSchema = z.object({
|
|
|
181
181
|
// API Response Schemas (for validating API responses)
|
|
182
182
|
// ============================================================================
|
|
183
183
|
/**
|
|
184
|
-
* User schema
|
|
184
|
+
* User schema — for validating a user payload from an API.
|
|
185
|
+
*
|
|
186
|
+
* Distinct from the layout `User` in `types/layout.ts`, which is a display
|
|
187
|
+
* view-model for the sidebar and avatar. This one describes data on the wire.
|
|
185
188
|
*/
|
|
186
189
|
export const userSchema = z.object({
|
|
187
190
|
id: z.string(),
|
|
@@ -9,7 +9,7 @@ export { emailSchema, passwordSchema, loginPasswordSchema, usernameSchema, fullN
|
|
|
9
9
|
export { loginSchema, registerSchema, forgotPasswordSchema, resetPasswordSchema, changePasswordSchema, mfaVerifySchema, mfaSetupSchema, profileUpdateSchema, emailChangeSchema, } from './auth.js';
|
|
10
10
|
export type { LoginFormData, RegisterFormData, ForgotPasswordFormData, ResetPasswordFormData, ChangePasswordFormData, MfaVerifyFormData, MfaSetupFormData, ProfileUpdateFormData, EmailChangeFormData, } from './auth.js';
|
|
11
11
|
export { userSchema, authTokensSchema, loginResponseSchema } from './auth.js';
|
|
12
|
-
export type {
|
|
12
|
+
export type { AuthUser, AuthTokens, LoginResponse } from './auth.js';
|
|
13
13
|
export { nonEmptyString, trimmedString, optionalString, urlSchema, optionalUrlSchema, slugSchema, } from './common.js';
|
|
14
14
|
export { positiveInt, nonNegativeInt, percentageSchema, priceSchema, numericString, } from './common.js';
|
|
15
15
|
export { isoDateSchema, dateStringSchema, timeStringSchema, futureDateSchema, pastDateSchema, } from './common.js';
|
|
@@ -122,8 +122,13 @@ declare class SidebarStore {
|
|
|
122
122
|
getPinnedIds(): string[];
|
|
123
123
|
/**
|
|
124
124
|
* Compute which sections should be initially expanded based on user roles.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
125
|
+
*
|
|
126
|
+
* A section listing `expandForRoles` expands for a user holding ANY of them.
|
|
127
|
+
* Sections without it fall back to the default: expanded unless explicitly
|
|
128
|
+
* collapsed.
|
|
129
|
+
*
|
|
130
|
+
* This is a UX affordance, not access control — it decides what starts open,
|
|
131
|
+
* not what is visible. Filtering is `filterNavItems`.
|
|
127
132
|
*/
|
|
128
133
|
computeInitialExpansions(sections: NavSection[], userRoles?: string[]): string[];
|
|
129
134
|
/**
|
|
@@ -17,6 +17,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
17
17
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
18
18
|
};
|
|
19
19
|
var _SidebarStore_instances, _SidebarStore_initialized, _SidebarStore_loadedFromStorage, _SidebarStore_loadedExpansionFromStorage, _SidebarStore_loadedPinnedFromStorage, _SidebarStore_onPinnedChange, _SidebarStore_appId, _SidebarStore_getStorageKey, _SidebarStore_persist, _SidebarStore_persistExpansion, _SidebarStore_persistPinned;
|
|
20
|
+
import { matchesAccess } from '../utils/nav-access.js';
|
|
20
21
|
const STORAGE_PREFIX = 'classic-theme-sidebar';
|
|
21
22
|
class SidebarStore {
|
|
22
23
|
constructor() {
|
|
@@ -281,15 +282,20 @@ class SidebarStore {
|
|
|
281
282
|
// ============================================
|
|
282
283
|
/**
|
|
283
284
|
* Compute which sections should be initially expanded based on user roles.
|
|
284
|
-
*
|
|
285
|
-
*
|
|
285
|
+
*
|
|
286
|
+
* A section listing `expandForRoles` expands for a user holding ANY of them.
|
|
287
|
+
* Sections without it fall back to the default: expanded unless explicitly
|
|
288
|
+
* collapsed.
|
|
289
|
+
*
|
|
290
|
+
* This is a UX affordance, not access control — it decides what starts open,
|
|
291
|
+
* not what is visible. Filtering is `filterNavItems`.
|
|
286
292
|
*/
|
|
287
293
|
computeInitialExpansions(sections, userRoles) {
|
|
288
294
|
return sections
|
|
289
295
|
.filter((section) => {
|
|
290
296
|
// Check role-based expansion first
|
|
291
297
|
if (section.expandForRoles?.length) {
|
|
292
|
-
return section.expandForRoles
|
|
298
|
+
return matchesAccess(section.expandForRoles, userRoles);
|
|
293
299
|
}
|
|
294
300
|
// Fall back to default behavior: expand unless explicitly collapsed
|
|
295
301
|
return !section.collapsible || section.expanded !== false;
|
|
@@ -78,8 +78,19 @@ export interface NavItem {
|
|
|
78
78
|
icon?: string;
|
|
79
79
|
/** Optional badge (notification count, etc.) */
|
|
80
80
|
badge?: string | number;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Roles that can see this item. The user needs ANY of them by default; set
|
|
83
|
+
* `requireAllRoles` on the Sidebar for ALL. Omit to show the item to everyone.
|
|
84
|
+
*/
|
|
82
85
|
roles?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* Permissions that can see this item. The user needs ANY of them by default;
|
|
88
|
+
* set `requireAllPermissions` on the Sidebar for ALL.
|
|
89
|
+
*
|
|
90
|
+
* Combined with `roles` as AND: an item listing both is shown only to a user
|
|
91
|
+
* who satisfies each.
|
|
92
|
+
*/
|
|
93
|
+
permissions?: string[];
|
|
83
94
|
/** Opens in new tab */
|
|
84
95
|
external?: boolean;
|
|
85
96
|
/** Whether this item is currently active */
|
|
@@ -125,7 +136,27 @@ export interface SidebarPreferences {
|
|
|
125
136
|
expandedItems: string[];
|
|
126
137
|
}
|
|
127
138
|
/**
|
|
128
|
-
* User data for layout components
|
|
139
|
+
* User data for layout components.
|
|
140
|
+
*
|
|
141
|
+
* A display view-model, deliberately not an identity: the layout only needs
|
|
142
|
+
* enough to render an avatar and decide what navigation to show, so this stays
|
|
143
|
+
* independent of any auth package. Apps map their own user into it.
|
|
144
|
+
*
|
|
145
|
+
* With `@classic-homes/auth`, derive the display fields rather than reaching for
|
|
146
|
+
* a matching shape — its `User` has no `name` or `initials`:
|
|
147
|
+
*
|
|
148
|
+
* ```typescript
|
|
149
|
+
* import { getDisplayName, getUserInitials } from '@classic-homes/auth/core';
|
|
150
|
+
*
|
|
151
|
+
* const layoutUser = {
|
|
152
|
+
* id: user.id,
|
|
153
|
+
* name: getDisplayName(user),
|
|
154
|
+
* initials: getUserInitials(user),
|
|
155
|
+
* email: user.email,
|
|
156
|
+
* roles: user.roles,
|
|
157
|
+
* permissions: user.permissions,
|
|
158
|
+
* };
|
|
159
|
+
* ```
|
|
129
160
|
*/
|
|
130
161
|
export interface User {
|
|
131
162
|
/** Unique identifier */
|
|
@@ -136,8 +167,16 @@ export interface User {
|
|
|
136
167
|
email?: string;
|
|
137
168
|
/** Avatar URL */
|
|
138
169
|
avatar?: string;
|
|
139
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Every role the user holds, used to filter navigation.
|
|
172
|
+
*
|
|
173
|
+
* A user may hold several; items are matched against the whole set, so a
|
|
174
|
+
* manager who is also an agent sees both. Not an access control boundary —
|
|
175
|
+
* this only decides what is rendered.
|
|
176
|
+
*/
|
|
140
177
|
roles?: string[];
|
|
178
|
+
/** Every permission the user holds, used to filter navigation. */
|
|
179
|
+
permissions?: string[];
|
|
141
180
|
/** User initials (fallback for avatar) */
|
|
142
181
|
initials?: string;
|
|
143
182
|
}
|
|
@@ -286,8 +325,17 @@ export interface PublicLayoutProps {
|
|
|
286
325
|
export interface SidebarProps {
|
|
287
326
|
/** Navigation sections */
|
|
288
327
|
navigation: NavSection[];
|
|
289
|
-
/**
|
|
328
|
+
/**
|
|
329
|
+
* Every role the current user holds. Items are matched against the whole set,
|
|
330
|
+
* so a user with several roles sees the union of what they allow.
|
|
331
|
+
*/
|
|
290
332
|
userRoles?: string[];
|
|
333
|
+
/** Every permission the current user holds, for permission-gated items. */
|
|
334
|
+
userPermissions?: string[];
|
|
335
|
+
/** Require ALL of an item's `roles` rather than any (default: any). */
|
|
336
|
+
requireAllRoles?: boolean;
|
|
337
|
+
/** Require ALL of an item's `permissions` rather than any (default: any). */
|
|
338
|
+
requireAllPermissions?: boolean;
|
|
291
339
|
/** Visual variant - light (default) or dark */
|
|
292
340
|
variant?: 'light' | 'dark';
|
|
293
341
|
/** Whether sidebar is collapsed */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Access Filtering
|
|
3
|
+
*
|
|
4
|
+
* Decides which navigation items a user should see, based on the roles and
|
|
5
|
+
* permissions they hold.
|
|
6
|
+
*
|
|
7
|
+
* A user holds a *set* of roles and a *set* of permissions, and an item is
|
|
8
|
+
* matched against the whole set — someone who is both a manager and an agent
|
|
9
|
+
* sees the union of what each allows.
|
|
10
|
+
*
|
|
11
|
+
* This decides what is **rendered**. It is not an access control boundary:
|
|
12
|
+
* the server enforces access, and hiding a link is a courtesy to the user, not
|
|
13
|
+
* a security measure.
|
|
14
|
+
*/
|
|
15
|
+
import type { NavItem } from '../types/layout.js';
|
|
16
|
+
/**
|
|
17
|
+
* Who is looking, and how strictly to match.
|
|
18
|
+
*/
|
|
19
|
+
export interface NavAccessContext {
|
|
20
|
+
/** Every role the user holds. */
|
|
21
|
+
userRoles?: string[];
|
|
22
|
+
/** Every permission the user holds. */
|
|
23
|
+
userPermissions?: string[];
|
|
24
|
+
/** Require ALL of an item's `roles` rather than any (default: any). */
|
|
25
|
+
requireAllRoles?: boolean;
|
|
26
|
+
/** Require ALL of an item's `permissions` rather than any (default: any). */
|
|
27
|
+
requireAllPermissions?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether `held` satisfies `required`.
|
|
31
|
+
*
|
|
32
|
+
* An empty or absent `required` is no restriction at all, so it matches. An
|
|
33
|
+
* empty `held` against a real restriction never matches — a user with no roles
|
|
34
|
+
* cannot satisfy a role requirement.
|
|
35
|
+
*/
|
|
36
|
+
export declare function matchesAccess(required: string[] | undefined, held: string[] | undefined, requireAll?: boolean): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a single item should be shown.
|
|
39
|
+
*
|
|
40
|
+
* `roles` and `permissions` combine as AND: an item declaring both is shown
|
|
41
|
+
* only to a user who satisfies each.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isNavItemVisible(item: NavItem, context?: NavAccessContext): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Filter a navigation tree.
|
|
46
|
+
*
|
|
47
|
+
* Recurses into `children`: a restricted child under an unrestricted parent is
|
|
48
|
+
* still checked, rather than inheriting the parent's visibility.
|
|
49
|
+
*
|
|
50
|
+
* A parent left with no children is dropped, since it would render as an empty
|
|
51
|
+
* expandable — unless it has its own `href`, which makes it a destination in
|
|
52
|
+
* its own right.
|
|
53
|
+
*
|
|
54
|
+
* Does not mutate `items`.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* filterNavItems(items, { userRoles: ['manager', 'agent'] });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function filterNavItems(items: NavItem[], context?: NavAccessContext): NavItem[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Access Filtering
|
|
3
|
+
*
|
|
4
|
+
* Decides which navigation items a user should see, based on the roles and
|
|
5
|
+
* permissions they hold.
|
|
6
|
+
*
|
|
7
|
+
* A user holds a *set* of roles and a *set* of permissions, and an item is
|
|
8
|
+
* matched against the whole set — someone who is both a manager and an agent
|
|
9
|
+
* sees the union of what each allows.
|
|
10
|
+
*
|
|
11
|
+
* This decides what is **rendered**. It is not an access control boundary:
|
|
12
|
+
* the server enforces access, and hiding a link is a courtesy to the user, not
|
|
13
|
+
* a security measure.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Whether `held` satisfies `required`.
|
|
17
|
+
*
|
|
18
|
+
* An empty or absent `required` is no restriction at all, so it matches. An
|
|
19
|
+
* empty `held` against a real restriction never matches — a user with no roles
|
|
20
|
+
* cannot satisfy a role requirement.
|
|
21
|
+
*/
|
|
22
|
+
export function matchesAccess(required, held, requireAll = false) {
|
|
23
|
+
if (!required || required.length === 0)
|
|
24
|
+
return true;
|
|
25
|
+
if (!held || held.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
return requireAll
|
|
28
|
+
? required.every((value) => held.includes(value))
|
|
29
|
+
: required.some((value) => held.includes(value));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a single item should be shown.
|
|
33
|
+
*
|
|
34
|
+
* `roles` and `permissions` combine as AND: an item declaring both is shown
|
|
35
|
+
* only to a user who satisfies each.
|
|
36
|
+
*/
|
|
37
|
+
export function isNavItemVisible(item, context = {}) {
|
|
38
|
+
return (matchesAccess(item.roles, context.userRoles, context.requireAllRoles) &&
|
|
39
|
+
matchesAccess(item.permissions, context.userPermissions, context.requireAllPermissions));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Filter a navigation tree.
|
|
43
|
+
*
|
|
44
|
+
* Recurses into `children`: a restricted child under an unrestricted parent is
|
|
45
|
+
* still checked, rather than inheriting the parent's visibility.
|
|
46
|
+
*
|
|
47
|
+
* A parent left with no children is dropped, since it would render as an empty
|
|
48
|
+
* expandable — unless it has its own `href`, which makes it a destination in
|
|
49
|
+
* its own right.
|
|
50
|
+
*
|
|
51
|
+
* Does not mutate `items`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* filterNavItems(items, { userRoles: ['manager', 'agent'] });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function filterNavItems(items, context = {}) {
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
if (!isNavItemVisible(item, context))
|
|
62
|
+
continue;
|
|
63
|
+
if (!item.children) {
|
|
64
|
+
result.push(item);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const children = filterNavItems(item.children, context);
|
|
68
|
+
if (item.children.length > 0 && children.length === 0 && !item.href) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
result.push({ ...item, children });
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -4,13 +4,3 @@ import { type ClassValue } from 'clsx';
|
|
|
4
4
|
* Uses clsx for conditional classes and tailwind-merge to handle conflicts
|
|
5
5
|
*/
|
|
6
6
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
7
|
-
/**
|
|
8
|
-
* Generate a unique ID with an optional prefix.
|
|
9
|
-
* Uses crypto.randomUUID() to avoid SSR hydration mismatches caused by
|
|
10
|
-
* sequential counters (where server and client counts can diverge).
|
|
11
|
-
* The short suffix is derived from the UUID for compact, readable IDs.
|
|
12
|
-
*
|
|
13
|
-
* @param prefix - Optional prefix for the ID (e.g., 'checkbox', 'tabs')
|
|
14
|
-
* @returns A unique ID string like 'checkbox-a1b2c3d4' or 'ct-a1b2c3d4'
|
|
15
|
-
*/
|
|
16
|
-
export declare function useId(prefix?: string): string;
|
package/dist/lib/utils.js
CHANGED
|
@@ -7,16 +7,3 @@ import { twMerge } from 'tailwind-merge';
|
|
|
7
7
|
export function cn(...inputs) {
|
|
8
8
|
return twMerge(clsx(inputs));
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Generate a unique ID with an optional prefix.
|
|
12
|
-
* Uses crypto.randomUUID() to avoid SSR hydration mismatches caused by
|
|
13
|
-
* sequential counters (where server and client counts can diverge).
|
|
14
|
-
* The short suffix is derived from the UUID for compact, readable IDs.
|
|
15
|
-
*
|
|
16
|
-
* @param prefix - Optional prefix for the ID (e.g., 'checkbox', 'tabs')
|
|
17
|
-
* @returns A unique ID string like 'checkbox-a1b2c3d4' or 'ct-a1b2c3d4'
|
|
18
|
-
*/
|
|
19
|
-
export function useId(prefix) {
|
|
20
|
-
const id = crypto.randomUUID().slice(0, 8);
|
|
21
|
-
return prefix ? `${prefix}-${id}` : `ct-${id}`;
|
|
22
|
-
}
|