@vc-shell/framework 1.2.2 → 1.2.3

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.
Files changed (33) hide show
  1. package/core/composables/useMenuService/index.ts +10 -2
  2. package/core/services/menu-service.ts +40 -1
  3. package/core/types/index.ts +47 -0
  4. package/dist/core/composables/useMenuService/index.d.ts +2 -2
  5. package/dist/core/composables/useMenuService/index.d.ts.map +1 -1
  6. package/dist/core/services/menu-service.d.ts +24 -1
  7. package/dist/core/services/menu-service.d.ts.map +1 -1
  8. package/dist/core/types/index.d.ts +34 -0
  9. package/dist/core/types/index.d.ts.map +1 -1
  10. package/dist/framework.js +5258 -5108
  11. package/dist/index.css +1 -1
  12. package/dist/shared/composables/index.d.ts +1 -0
  13. package/dist/shared/composables/index.d.ts.map +1 -1
  14. package/dist/shared/composables/useTableSelection.d.ts +57 -0
  15. package/dist/shared/composables/useTableSelection.d.ts.map +1 -0
  16. package/dist/tsconfig.tsbuildinfo +1 -1
  17. package/dist/ui/components/atoms/vc-badge/vc-badge.vue.d.ts +3 -0
  18. package/dist/ui/components/atoms/vc-badge/vc-badge.vue.d.ts.map +1 -1
  19. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/useBadge.d.ts +18 -0
  20. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/useBadge.d.ts.map +1 -0
  21. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/vc-app-menu-link.vue.d.ts +4 -1
  22. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/vc-app-menu-link.vue.d.ts.map +1 -1
  23. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts +4 -1
  24. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts.map +1 -1
  25. package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue.d.ts.map +1 -1
  26. package/package.json +5 -5
  27. package/shared/composables/index.ts +1 -0
  28. package/shared/composables/useTableSelection.ts +145 -0
  29. package/ui/components/atoms/vc-badge/vc-badge.vue +59 -0
  30. package/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/useBadge.ts +80 -0
  31. package/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/vc-app-menu-link.vue +53 -10
  32. package/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue +10 -1
  33. package/ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue +3 -0
@@ -9,6 +9,8 @@ export interface Props {
9
9
  customPosition?: boolean;
10
10
  top?: string;
11
11
  right?: string;
12
+ /** When true, renders badge as inline element without absolute positioning (no slot content) */
13
+ inline?: boolean;
12
14
  }
13
15
  export interface Emits {
14
16
  (event: "click"): void;
@@ -30,6 +32,7 @@ declare const __VLS_component: import("vue").DefineComponent<Props, {}, {}, {},
30
32
  customPosition: boolean;
31
33
  top: string;
32
34
  right: string;
35
+ inline: boolean;
33
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
37
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
35
38
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"vc-badge.vue.d.ts","sourceRoot":"","sources":["../../../../../ui/components/atoms/vc-badge/vc-badge.vue"],"names":[],"mappings":"AAmPA,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACxB;AAeD,KAAK,WAAW,GAAG;IACjB;;SAEK;IACL,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC;CAC9B,CAAC;AAyGF,QAAA,MAAM,eAAe;;;;;UAvIZ,GAAG,GAAG,GAAG;WACR,OAAO;aACL,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW;oBAC5D,OAAO;SAClB,MAAM;WACJ,MAAM;6EA0Id,CAAC;wBACkB,eAAe,CAAC,OAAO,eAAe,EAAE,WAAW,CAAC;AAAzE,wBAA0E;AAa1E,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KAEV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"vc-badge.vue.d.ts","sourceRoot":"","sources":["../../../../../ui/components/atoms/vc-badge/vc-badge.vue"],"names":[],"mappings":"AA8SA,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gGAAgG;IAChG,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACxB;AAgBD,KAAK,WAAW,GAAG;IACjB;;SAEK;IACL,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC;CAC9B,CAAC;AAyIF,QAAA,MAAM,eAAe;;;;;UA1KZ,GAAG,GAAG,GAAG;WACR,OAAO;aACL,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW;oBAC5D,OAAO;SAClB,MAAM;WACJ,MAAM;YAEL,OAAO;6EA2KhB,CAAC;wBACkB,eAAe,CAAC,OAAO,eAAe,EAAE,WAAW,CAAC;AAAzE,wBAA0E;AAa1E,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KAEV,CAAA;CACD,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type ComputedRef } from "vue";
2
+ import type { MenuItemBadgeConfig } from "./../../../../../../../../../core/types";
3
+ export interface ResolvedBadge {
4
+ content: string | number | undefined;
5
+ variant: "primary" | "success" | "warning" | "danger" | "info" | "secondary";
6
+ isDot: boolean;
7
+ isVisible: boolean;
8
+ }
9
+ /**
10
+ * Composable to resolve badge configuration for menu items.
11
+ * Supports direct badge config, routeId lookup, or groupId lookup.
12
+ *
13
+ * @param badgeConfig - Direct badge configuration (takes priority)
14
+ * @param routeId - Route ID to lookup badge from registry
15
+ * @param groupId - Group ID to lookup badge from registry
16
+ */
17
+ export declare function useBadge(badgeConfig: MenuItemBadgeConfig | undefined, routeId?: string, groupId?: string): ComputedRef<ResolvedBadge>;
18
+ //# sourceMappingURL=useBadge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBadge.d.ts","sourceRoot":"","sources":["../../../../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/useBadge.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,WAAW,EAAY,MAAM,KAAK,CAAC;AACzE,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,yCAAyC,CAAC;AAGlG,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IAC7E,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,WAAW,EAAE,mBAAmB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,WAAW,CAAC,aAAa,CAAC,CAwD5B"}
@@ -1,5 +1,5 @@
1
1
  import { Component } from "vue";
2
- import { MenuItem } from "./../../../../../../../../../core/types";
2
+ import { MenuItem, MenuItemBadgeConfig } from "./../../../../../../../../../core/types";
3
3
  export interface Props {
4
4
  children?: MenuItem[];
5
5
  sticky?: boolean;
@@ -8,6 +8,9 @@ export interface Props {
8
8
  url?: string;
9
9
  expand?: boolean;
10
10
  id?: string | number;
11
+ badge?: MenuItemBadgeConfig;
12
+ routeId?: string;
13
+ groupId?: string;
11
14
  }
12
15
  export interface Emits {
13
16
  (event: "onClick", item?: MenuItem): void;
@@ -1 +1 @@
1
- {"version":3,"file":"vc-app-menu-link.vue.d.ts","sourceRoot":"","sources":["../../../../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/vc-app-menu-link.vue"],"names":[],"mappings":"AAAA,OAqZO,EAAmC,SAAS,EAAmB,MAAM,KAAK,CAAC;AAClF,OAAO,EAAE,QAAQ,EAAE,MAAM,yCAAyC,CAAC;AAInE,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3C;;;;;;YAVU,OAAO;;AAiVlB,wBAQG"}
1
+ {"version":3,"file":"vc-app-menu-link.vue.d.ts","sourceRoot":"","sources":["../../../../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/vc-app-menu-link.vue"],"names":[],"mappings":"AAAA,OAgcO,EAAmC,SAAS,EAAmB,MAAM,KAAK,CAAC;AAClF,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAKxF,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3C;;;;;;YAbU,OAAO;;AA+YlB,wBAQG"}
@@ -1,4 +1,4 @@
1
- import { MenuItem } from "../../../../../../../../core/types";
1
+ import { MenuItem, MenuItemBadgeConfig } from "../../../../../../../../core/types";
2
2
  import type { Component } from "vue";
3
3
  export interface Props {
4
4
  sticky?: boolean;
@@ -9,6 +9,9 @@ export interface Props {
9
9
  children?: MenuItem[];
10
10
  expand?: boolean;
11
11
  id?: string | number;
12
+ badge?: MenuItemBadgeConfig;
13
+ routeId?: string;
14
+ groupId?: string;
12
15
  }
13
16
  export interface Emits {
14
17
  (event: "click", item?: MenuItem): void;
@@ -1 +1 @@
1
- {"version":3,"file":"vc-app-menu-item.vue.d.ts","sourceRoot":"","sources":["../../../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue"],"names":[],"mappings":"AAgEA,OAAO,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CACzC;;;;;;cAPY,QAAQ,EAAE;YALZ,OAAO;;AA8IlB,wBAQG"}
1
+ {"version":3,"file":"vc-app-menu-item.vue.d.ts","sourceRoot":"","sources":["../../../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue"],"names":[],"mappings":"AAyEA,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AACnF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CACzC;;;;;;cAVY,QAAQ,EAAE;YALZ,OAAO;;AA6JlB,wBAQG"}
@@ -1 +1 @@
1
- {"version":3,"file":"vc-app-menu.vue.d.ts","sourceRoot":"","sources":["../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue"],"names":[],"mappings":"AAiPA,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAIxD,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CAChC;;;;;;;;aANU,MAAM,GAAG,SAAS;;AA4J7B,wBAQG"}
1
+ {"version":3,"file":"vc-app-menu.vue.d.ts","sourceRoot":"","sources":["../../../../../../../ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue"],"names":[],"mappings":"AAoPA,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAIxD,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CAChC;;;;;;;;aANU,MAAM,GAAG,SAAS;;AAkK7B,wBAQG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/framework",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "type": "module",
5
5
  "main": "./dist/framework.js",
6
6
  "types": "./dist/index.d.ts",
@@ -90,9 +90,9 @@
90
90
  "@fullhuman/postcss-purgecss": "^7.0.2",
91
91
  "@laynezh/vite-plugin-lib-assets": "v1.1.0",
92
92
  "@types/dompurify": "^3.0.5",
93
- "@vc-shell/api-client-generator": "1.2.2",
94
- "@vc-shell/config-generator": "1.2.2",
95
- "@vc-shell/ts-config": "1.2.2",
93
+ "@vc-shell/api-client-generator": "1.2.3",
94
+ "@vc-shell/config-generator": "1.2.3",
95
+ "@vc-shell/ts-config": "1.2.3",
96
96
  "@vitejs/plugin-vue": "^5.2.3",
97
97
  "@vue/test-utils": "^2.4.5",
98
98
  "cypress-signalr-mock": "^1.5.0",
@@ -112,5 +112,5 @@
112
112
  "access": "public",
113
113
  "registry": "https://registry.npmjs.org/"
114
114
  },
115
- "gitHead": "c969535a7f96eeb66aa9ab2289c369286d8933fd"
115
+ "gitHead": "6270dab6a5a8149c8380d17d55b77531bad13531"
116
116
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./useMenuExpanded";
2
2
  export * from "./useModificationTracker";
3
3
  export * from "./useTableSort";
4
+ export * from "./useTableSelection";
4
5
  export * from "./useExternalWidgets";
@@ -0,0 +1,145 @@
1
+ import { ref, computed, Ref, ComputedRef } from "vue";
2
+
3
+ export interface UseTableSelectionOptions<T> {
4
+ /**
5
+ * The field to use for extracting IDs from items.
6
+ * Can be a key of T or a function that extracts the ID.
7
+ * @default 'id'
8
+ */
9
+ idField?: keyof T | ((item: T) => string | undefined);
10
+ }
11
+
12
+ export interface UseTableSelectionReturn<T> {
13
+ /**
14
+ * Array of currently selected item objects.
15
+ */
16
+ selectedItems: Ref<T[]>;
17
+
18
+ /**
19
+ * Computed array of IDs extracted from selected items.
20
+ */
21
+ selectedIds: ComputedRef<string[]>;
22
+
23
+ /**
24
+ * Whether "select all" across pagination is active.
25
+ */
26
+ allSelected: Ref<boolean>;
27
+
28
+ /**
29
+ * Number of currently selected items.
30
+ */
31
+ selectionCount: ComputedRef<number>;
32
+
33
+ /**
34
+ * Whether any items are currently selected.
35
+ */
36
+ hasSelection: ComputedRef<boolean>;
37
+
38
+ /**
39
+ * Handler for VcTable's @selection-changed event.
40
+ */
41
+ handleSelectionChange: (items: T[]) => void;
42
+
43
+ /**
44
+ * Handler for VcTable's @select:all event.
45
+ */
46
+ handleSelectAll: (selected: boolean) => void;
47
+
48
+ /**
49
+ * Clears all selection state.
50
+ */
51
+ resetSelection: () => void;
52
+
53
+ /**
54
+ * Checks if a specific item is selected.
55
+ */
56
+ isSelected: (item: T) => boolean;
57
+
58
+ /**
59
+ * Programmatically select items.
60
+ */
61
+ selectItems: (items: T[]) => void;
62
+
63
+ /**
64
+ * Deselect items by their IDs.
65
+ */
66
+ deselectByIds: (ids: string[]) => void;
67
+ }
68
+
69
+ export function useTableSelection<T extends object>(
70
+ options?: UseTableSelectionOptions<T>
71
+ ): UseTableSelectionReturn<T> {
72
+ const idField = options?.idField ?? ("id" as keyof T);
73
+
74
+ const getItemId = (item: T): string | undefined => {
75
+ if (typeof idField === "function") {
76
+ return idField(item);
77
+ }
78
+ const value = item[idField as keyof T];
79
+ return typeof value === "string" ? value : undefined;
80
+ };
81
+
82
+ const selectedItems = ref<T[]>([]) as Ref<T[]>;
83
+ const allSelected = ref(false);
84
+
85
+ const selectedIds = computed<string[]>(() => {
86
+ return selectedItems.value.flatMap((item) => {
87
+ const id = getItemId(item);
88
+ return id ? [id] : [];
89
+ });
90
+ });
91
+
92
+ const selectionCount = computed(() => selectedItems.value.length);
93
+ const hasSelection = computed(() => selectedItems.value.length > 0);
94
+ const selectedIdSet = computed(() => new Set(selectedIds.value));
95
+
96
+ const handleSelectionChange = (items: T[]): void => {
97
+ selectedItems.value = items;
98
+ if (items.length === 0) {
99
+ allSelected.value = false;
100
+ }
101
+ };
102
+
103
+ const handleSelectAll = (selected: boolean): void => {
104
+ allSelected.value = selected;
105
+ if (!selected) {
106
+ selectedItems.value = [];
107
+ }
108
+ };
109
+
110
+ const resetSelection = (): void => {
111
+ selectedItems.value = [];
112
+ allSelected.value = false;
113
+ };
114
+
115
+ const isSelected = (item: T): boolean => {
116
+ const id = getItemId(item);
117
+ return id ? selectedIdSet.value.has(id) : false;
118
+ };
119
+
120
+ const selectItems = (items: T[]): void => {
121
+ selectedItems.value = items;
122
+ };
123
+
124
+ const deselectByIds = (ids: string[]): void => {
125
+ const idsToRemove = new Set(ids);
126
+ selectedItems.value = selectedItems.value.filter((item) => {
127
+ const id = getItemId(item);
128
+ return id ? !idsToRemove.has(id) : true;
129
+ });
130
+ };
131
+
132
+ return {
133
+ selectedItems,
134
+ selectedIds,
135
+ allSelected,
136
+ selectionCount,
137
+ hasSelection,
138
+ handleSelectionChange,
139
+ handleSelectAll,
140
+ resetSelection,
141
+ isSelected,
142
+ selectItems,
143
+ deselectByIds,
144
+ };
145
+ }
@@ -1,5 +1,34 @@
1
1
  <template>
2
+ <!-- Inline mode: render badge element directly without wrapper -->
2
3
  <div
4
+ v-if="inline && (typeof content !== 'undefined' || isDot)"
5
+ ref="badge"
6
+ class="vc-badge__badge vc-badge__badge--inline"
7
+ :class="[
8
+ `vc-badge__badge--${variant}`,
9
+ {
10
+ 'vc-badge__badge--active': active,
11
+ 'vc-badge__badge--clickable': clickable,
12
+ 'vc-badge__badge--disabled': disabled,
13
+ 'vc-badge__badge--content-long': String(content).length > 1,
14
+ 'vc-badge__badge--content-very-long': String(content).length > 2,
15
+ 'vc-badge__badge--dot': isDot,
16
+ 'vc-badge__badge--inline-small': size === 's',
17
+ 'vc-badge__badge--inline-medium': size === 'm',
18
+ },
19
+ ]"
20
+ @click="onClick"
21
+ >
22
+ <span
23
+ v-if="!isDot"
24
+ class="vc-badge__text"
25
+ >{{ content }}</span
26
+ >
27
+ </div>
28
+
29
+ <!-- Standard mode: wrapper with slot and positioned badge -->
30
+ <div
31
+ v-else
3
32
  ref="badgeContainer"
4
33
  class="vc-badge"
5
34
  :class="{
@@ -53,6 +82,8 @@ export interface Props {
53
82
  customPosition?: boolean;
54
83
  top?: string;
55
84
  right?: string;
85
+ /** When true, renders badge as inline element without absolute positioning (no slot content) */
86
+ inline?: boolean;
56
87
  }
57
88
 
58
89
  export interface Emits {
@@ -66,6 +97,7 @@ const props = withDefaults(defineProps<Props>(), {
66
97
  customPosition: false,
67
98
  top: undefined,
68
99
  right: undefined,
100
+ inline: false,
69
101
  });
70
102
 
71
103
  const emit = defineEmits<Emits>();
@@ -236,4 +268,31 @@ $sizes: small, medium;
236
268
  .vc-badge__badge--disabled {
237
269
  @apply tw-cursor-not-allowed tw-bg-[color:var(--badge-background-color-disabled)] tw-text-[color:var(--badge-text-color-disabled)] tw-border-[color:var(--badge-border-color-disabled)] hover:tw-bg-[color:var(--badge-background-color-disabled)] hover:tw-text-[color:var(--badge-text-color-disabled)] hover:tw-border-[color:var(--badge-border-color-disabled)];
238
270
  }
271
+
272
+ // Inline mode - relative positioning, no wrapper
273
+ .vc-badge__badge--inline {
274
+ @apply tw-relative tw-top-0 tw-right-0;
275
+ }
276
+
277
+ .vc-badge__badge--inline-small {
278
+ height: var(--badge-size-small);
279
+ min-width: var(--badge-size-small);
280
+
281
+ &.vc-badge__badge--dot {
282
+ height: var(--badge-dot-size-small);
283
+ min-width: var(--badge-dot-size-small);
284
+ width: var(--badge-dot-size-small);
285
+ }
286
+ }
287
+
288
+ .vc-badge__badge--inline-medium {
289
+ height: var(--badge-size-medium);
290
+ min-width: var(--badge-size-medium);
291
+
292
+ &.vc-badge__badge--dot {
293
+ height: var(--badge-dot-size-medium);
294
+ min-width: var(--badge-dot-size-medium);
295
+ width: var(--badge-dot-size-medium);
296
+ }
297
+ }
239
298
  </style>
@@ -0,0 +1,80 @@
1
+ import { computed, unref, isRef, type ComputedRef, type Ref } from "vue";
2
+ import type { MenuItemBadgeConfig, MenuItemBadge } from "./../../../../../../../../../core/types";
3
+ import { getMenuBadges } from "./../../../../../../../../../core/services/menu-service";
4
+
5
+ export interface ResolvedBadge {
6
+ content: string | number | undefined;
7
+ variant: "primary" | "success" | "warning" | "danger" | "info" | "secondary";
8
+ isDot: boolean;
9
+ isVisible: boolean;
10
+ }
11
+
12
+ /**
13
+ * Composable to resolve badge configuration for menu items.
14
+ * Supports direct badge config, routeId lookup, or groupId lookup.
15
+ *
16
+ * @param badgeConfig - Direct badge configuration (takes priority)
17
+ * @param routeId - Route ID to lookup badge from registry
18
+ * @param groupId - Group ID to lookup badge from registry
19
+ */
20
+ export function useBadge(
21
+ badgeConfig: MenuItemBadgeConfig | undefined,
22
+ routeId?: string,
23
+ groupId?: string,
24
+ ): ComputedRef<ResolvedBadge> {
25
+ const menuBadges = getMenuBadges();
26
+
27
+ return computed(() => {
28
+ // Priority: direct badge config > badge from registry by routeId > badge from registry by groupId
29
+ let config: MenuItemBadgeConfig | undefined = badgeConfig;
30
+ if (!config && routeId) {
31
+ config = menuBadges.value.get(routeId);
32
+ }
33
+ if (!config && groupId) {
34
+ config = menuBadges.value.get(groupId);
35
+ }
36
+
37
+ if (!config) {
38
+ return { content: undefined, variant: "primary", isDot: false, isVisible: false };
39
+ }
40
+
41
+ // Normalize shorthand to full config object
42
+ let badge: MenuItemBadge;
43
+ if (typeof config === "object" && !isRef(config) && "content" in config) {
44
+ badge = config as MenuItemBadge;
45
+ } else {
46
+ badge = { content: config as MenuItemBadge["content"] };
47
+ }
48
+
49
+ // Resolve reactive content
50
+ let rawContent: string | number | undefined = undefined;
51
+ if (typeof badge.content === "function") {
52
+ rawContent = badge.content();
53
+ } else if (isRef(badge.content)) {
54
+ rawContent = unref(badge.content as Ref<string | number | undefined>);
55
+ } else {
56
+ rawContent = badge.content;
57
+ }
58
+
59
+ // Truncate values > 99
60
+ let displayContent: string | number | undefined = rawContent;
61
+ if (typeof rawContent === "number" && rawContent > 99) {
62
+ displayContent = "99+";
63
+ } else if (typeof rawContent === "string") {
64
+ const numValue = parseInt(rawContent, 10);
65
+ if (!isNaN(numValue) && numValue > 99) {
66
+ displayContent = "99+";
67
+ }
68
+ }
69
+
70
+ const isDot = badge.isDot ?? false;
71
+ const isVisible = isDot || (displayContent != null && displayContent !== "" && displayContent !== 0);
72
+
73
+ return {
74
+ content: displayContent,
75
+ variant: badge.variant ?? "primary",
76
+ isDot,
77
+ isVisible,
78
+ };
79
+ });
80
+ }
@@ -45,6 +45,15 @@
45
45
  <div class="vc-app-menu-link__title-truncate">
46
46
  {{ $t(title ?? "") }}
47
47
  </div>
48
+ <VcBadge
49
+ v-if="resolvedBadge.isVisible"
50
+ :content="resolvedBadge.isDot ? undefined : resolvedBadge.content"
51
+ :variant="resolvedBadge.variant"
52
+ :is-dot="resolvedBadge.isDot"
53
+ size="s"
54
+ inline
55
+ class="vc-app-menu-link__badge"
56
+ />
48
57
  <div
49
58
  v-if="(!!children?.length && expand) || false"
50
59
  class="vc-app-menu-link__title-icon"
@@ -119,15 +128,28 @@
119
128
  {{ twoLettersTitle(nested.title) }}
120
129
  </div>
121
130
  <Transition name="opacity">
122
- <p
131
+ <div
123
132
  v-show="expand"
124
- class="vc-app-menu-link__child-item-title"
125
- :class="{
126
- 'vc-app-menu-link__child-item-title--no-icon': !nested.icon,
127
- }"
133
+ class="vc-app-menu-link__child-item-content"
128
134
  >
129
- {{ $t(nested.title) }}
130
- </p>
135
+ <p
136
+ class="vc-app-menu-link__child-item-title"
137
+ :class="{
138
+ 'vc-app-menu-link__child-item-title--no-icon': !nested.icon,
139
+ }"
140
+ >
141
+ {{ $t(nested.title) }}
142
+ </p>
143
+ <VcBadge
144
+ v-if="getChildBadge(nested).isVisible"
145
+ :content="getChildBadge(nested).isDot ? undefined : getChildBadge(nested).content"
146
+ :variant="getChildBadge(nested).variant"
147
+ :is-dot="getChildBadge(nested).isDot"
148
+ size="s"
149
+ inline
150
+ class="vc-app-menu-link__badge"
151
+ />
152
+ </div>
131
153
  </Transition>
132
154
  </div>
133
155
  </div>
@@ -139,9 +161,10 @@
139
161
 
140
162
  <script lang="ts" setup>
141
163
  import { ref, watch, computed, onMounted, Component, MaybeRef, unref } from "vue";
142
- import { MenuItem } from "./../../../../../../../../../core/types";
143
- import { VcIcon } from "./../../../../../../../";
164
+ import { MenuItem, MenuItemBadgeConfig } from "./../../../../../../../../../core/types";
165
+ import { VcIcon, VcBadge } from "./../../../../../../../";
144
166
  import { useRoute } from "vue-router";
167
+ import { useBadge } from "./useBadge";
145
168
 
146
169
  export interface Props {
147
170
  children?: MenuItem[];
@@ -151,6 +174,9 @@ export interface Props {
151
174
  url?: string;
152
175
  expand?: boolean;
153
176
  id?: string | number;
177
+ badge?: MenuItemBadgeConfig;
178
+ routeId?: string;
179
+ groupId?: string;
154
180
  }
155
181
 
156
182
  export interface Emits {
@@ -163,6 +189,14 @@ const props = withDefaults(defineProps<Props>(), {
163
189
 
164
190
  const emit = defineEmits<Emits>();
165
191
 
192
+ // Badge resolution for parent menu item
193
+ const resolvedBadge = useBadge(props.badge, props.routeId, props.groupId);
194
+
195
+ // Helper to get resolved badge for child items
196
+ const getChildBadge = (child: MenuItem) => {
197
+ return useBadge(child.badge, child.routeId, child.groupId).value;
198
+ };
199
+
166
200
  const isOpened = ref(false);
167
201
  const route = useRoute();
168
202
  const params = Object.fromEntries(Object.entries(route.params).filter(([key]) => key !== "pathMatch"));
@@ -346,7 +380,7 @@ watch(isOpened, (newValue) => {
346
380
 
347
381
  &__child-item {
348
382
  @apply tw-cursor-pointer tw-min-w-0 tw-flex tw-h-[var(--app-menu-item-height)]
349
- tw-items-center tw-transition-[padding] tw-duration-150 tw-w-full tw-py-2 tw-px-3
383
+ tw-items-center tw-transition-[padding] tw-duration-150 tw-w-full tw-py-2 tw-pl-3 tw-pr-2
350
384
  tw-text-[color:var(--app-menu-item-title-color)] tw-text-sm
351
385
  hover:tw-bg-[var(--app-menu-item-background-color-hover)]
352
386
  hover:tw-text-[color:var(--app-menu-item-title-color-active)] tw-gap-5;
@@ -389,6 +423,15 @@ watch(isOpened, (newValue) => {
389
423
  &__icon-collapsed {
390
424
  @apply tw-p-0 tw-m-0;
391
425
  }
426
+
427
+ // Badge styles for menu items - positioned after text
428
+ &__badge {
429
+ @apply tw-shrink-0 tw-ml-auto;
430
+ }
431
+
432
+ &__child-item-content {
433
+ @apply tw-flex tw-items-center tw-w-full tw-min-w-0;
434
+ }
392
435
  }
393
436
 
394
437
  .opacity-enter-active,
@@ -13,6 +13,9 @@
13
13
  :title="title ?? ''"
14
14
  :url="url"
15
15
  :expand="expand"
16
+ :badge="badge"
17
+ :route-id="routeId"
18
+ :group-id="groupId"
16
19
  @on-click="$emit('click')"
17
20
  />
18
21
  </router-link>
@@ -26,6 +29,9 @@
26
29
  :icon="icon ?? ''"
27
30
  :title="title ?? ''"
28
31
  :expand="expand"
32
+ :badge="badge"
33
+ :route-id="routeId"
34
+ :group-id="groupId"
29
35
  @on-click="$emit('click', $event)"
30
36
  />
31
37
  </template>
@@ -34,7 +40,7 @@
34
40
 
35
41
  <script lang="ts" setup>
36
42
  import VcAppMenuLink from "./_internal/vc-app-menu-link.vue";
37
- import { MenuItem } from "../../../../../../../../core/types";
43
+ import { MenuItem, MenuItemBadgeConfig } from "../../../../../../../../core/types";
38
44
  import type { Component } from "vue";
39
45
  export interface Props {
40
46
  sticky?: boolean;
@@ -45,6 +51,9 @@ export interface Props {
45
51
  children?: MenuItem[];
46
52
  expand?: boolean;
47
53
  id?: string | number;
54
+ badge?: MenuItemBadgeConfig;
55
+ routeId?: string;
56
+ groupId?: string;
48
57
  }
49
58
 
50
59
  export interface Emits {
@@ -36,6 +36,9 @@
36
36
  :title="item.title as string"
37
37
  :children="item.children"
38
38
  :expand="$isDesktop.value ? isExpanded || isHoverExpanded : true"
39
+ :badge="item.badge"
40
+ :route-id="item.routeId"
41
+ :group-id="item.groupId"
39
42
  @click="handleMenuItemClick(item, $event)"
40
43
  />
41
44
  </div>