@vc-shell/framework 1.2.4-beta.5 → 1.2.4-beta.6

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 (57) hide show
  1. package/core/utilities/date/formatDate.ts +1 -1
  2. package/dist/{DashboardBarChart-B-g_a-7F.js → DashboardBarChart-BzfKkUke.js} +1 -1
  3. package/dist/{DashboardDonutChart-AktPFUNo.js → DashboardDonutChart-CWfe85Xq.js} +1 -1
  4. package/dist/{DashboardLineChart-BQKqRFhM.js → DashboardLineChart-kdA8VnrR.js} +1 -1
  5. package/dist/{GridstackDashboard-BXqCpiMw.js → GridstackDashboard-CGHYkReX.js} +1 -1
  6. package/dist/framework.js +1 -1
  7. package/dist/{index-DVyGELzS.js → index-tgmgQAD9.js} +6839 -6798
  8. package/dist/index.css +1 -1
  9. package/dist/locales/de.json +2 -0
  10. package/dist/locales/en.json +2 -0
  11. package/dist/shared/components/user-dropdown-button/_internal/user-info.vue.d.ts.map +1 -1
  12. package/dist/shared/pages/_storybook-helpers.d.ts +11 -0
  13. package/dist/shared/pages/_storybook-helpers.d.ts.map +1 -0
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/dist/ui/components/organisms/vc-app/_internal/menu/VcAppMenu.vue.d.ts.map +1 -1
  16. package/dist/ui/components/organisms/vc-blade/_internal/toolbar/ToolbarMobile.vue.d.ts.map +1 -1
  17. package/dist/ui/components/organisms/vc-popup/vc-popup.vue.d.ts.map +1 -1
  18. package/dist/ui/components/organisms/vc-sidebar/vc-sidebar.vue.d.ts.map +1 -1
  19. package/dist/ui/components/organisms/vc-table/VcDataTable.vue.d.ts +5 -2
  20. package/dist/ui/components/organisms/vc-table/VcDataTable.vue.d.ts.map +1 -1
  21. package/dist/ui/components/organisms/vc-table/VcTableAdapter.vue.d.ts.map +1 -1
  22. package/dist/ui/components/organisms/vc-table/base/BaseVcDataTable.d.ts +9 -1
  23. package/dist/ui/components/organisms/vc-table/base/BaseVcDataTable.d.ts.map +1 -1
  24. package/dist/ui/components/organisms/vc-table/components/DataTableBody.vue.d.ts +7 -0
  25. package/dist/ui/components/organisms/vc-table/components/DataTableBody.vue.d.ts.map +1 -1
  26. package/dist/ui/components/organisms/vc-table/components/DataTableRow.vue.d.ts +2 -0
  27. package/dist/ui/components/organisms/vc-table/components/DataTableRow.vue.d.ts.map +1 -1
  28. package/dist/ui/components/organisms/vc-table/components/TableEmpty.vue.d.ts +7 -9
  29. package/dist/ui/components/organisms/vc-table/components/TableEmpty.vue.d.ts.map +1 -1
  30. package/dist/ui/components/organisms/vc-table/components/TableRow.vue.d.ts +4 -0
  31. package/dist/ui/components/organisms/vc-table/components/TableRow.vue.d.ts.map +1 -1
  32. package/dist/ui/components/organisms/vc-table/types.d.ts +21 -0
  33. package/dist/ui/components/organisms/vc-table/types.d.ts.map +1 -1
  34. package/dist/{vc-editor-BtJrxrBg.js → vc-editor-BNrG1GAG.js} +1 -1
  35. package/dist/{vc-slider-sUKMaKnc.js → vc-slider-Ce0X1_1m.js} +1 -1
  36. package/package.json +5 -5
  37. package/shared/components/user-dropdown-button/_internal/user-info.vue +26 -13
  38. package/shared/pages/ChangePasswordPage/components/change-password/change-password.stories.ts +44 -0
  39. package/shared/pages/ForgotPasswordPage/components/forgot-password/forgot-password.stories.ts +38 -0
  40. package/shared/pages/InvitePage/components/invite/invite.stories.ts +64 -0
  41. package/shared/pages/LoginPage/components/login/login.stories.ts +52 -0
  42. package/shared/pages/ResetPasswordPage/components/reset-password/reset-password.stories.ts +64 -0
  43. package/shared/pages/_storybook-helpers.ts +71 -0
  44. package/ui/components/molecules/vc-select/vc-select.vue +1 -1
  45. package/ui/components/organisms/vc-app/_internal/menu/VcAppMenu.vue +1 -2
  46. package/ui/components/organisms/vc-blade/_internal/toolbar/ToolbarMobile.vue +18 -22
  47. package/ui/components/organisms/vc-popup/vc-popup.vue +3 -4
  48. package/ui/components/organisms/vc-sidebar/vc-sidebar.vue +12 -10
  49. package/ui/components/organisms/vc-table/VcDataTable.vue +58 -11
  50. package/ui/components/organisms/vc-table/VcTableAdapter.vue +31 -84
  51. package/ui/components/organisms/vc-table/base/BaseVcDataTable.ts +10 -0
  52. package/ui/components/organisms/vc-table/components/DataTableBody.vue +10 -0
  53. package/ui/components/organisms/vc-table/components/DataTableRow.vue +3 -0
  54. package/ui/components/organisms/vc-table/components/TableEmpty.vue +18 -12
  55. package/ui/components/organisms/vc-table/components/TableRow.vue +5 -1
  56. package/ui/components/organisms/vc-table/types.ts +22 -0
  57. package/ui/components/organisms/vc-table/vc-data-table.stories.ts +205 -0
@@ -0,0 +1,64 @@
1
+ import { onUnmounted } from "vue";
2
+ import type { Meta, StoryObj } from "@storybook/vue3-vite";
3
+ import Invite from "./Invite.vue";
4
+ import { useUserManagement } from "@core/composables/useUserManagement";
5
+ import { mockPlatformApiFetch, patchUserManagement } from "@shared/pages/_storybook-helpers";
6
+
7
+ const meta = {
8
+ title: "Shared/Pages/Invite",
9
+ component: Invite,
10
+ tags: ["autodocs"],
11
+ decorators: [
12
+ () => ({
13
+ setup() {
14
+ mockPlatformApiFetch();
15
+ patchUserManagement();
16
+ },
17
+ template: "<story />",
18
+ }),
19
+ ],
20
+ args: {
21
+ userId: "mock-user-id",
22
+ userName: "john@example.com",
23
+ token: "mock-invite-token",
24
+ },
25
+ parameters: {
26
+ layout: "fullscreen",
27
+ docs: {
28
+ description: {
29
+ component:
30
+ "Invitation acceptance page. User sets a password for their invited account. Validates token on mount, then shows password/confirm fields. Auto-signs in on success.",
31
+ },
32
+ },
33
+ },
34
+ } satisfies Meta<typeof Invite>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ export const Default: Story = {};
40
+
41
+ export const WithCustomBackground: Story = {
42
+ args: {
43
+ background: "https://images.unsplash.com/photo-1557683316-973673baf926?w=1920&q=80",
44
+ },
45
+ };
46
+
47
+ export const InvalidToken: Story = {
48
+ args: {
49
+ token: "expired-token",
50
+ },
51
+ decorators: [
52
+ () => ({
53
+ setup() {
54
+ const userManagement = useUserManagement();
55
+ const orig = userManagement.validateToken;
56
+ userManagement.validateToken = async () => false;
57
+ onUnmounted(() => {
58
+ userManagement.validateToken = orig;
59
+ });
60
+ },
61
+ template: "<story />",
62
+ }),
63
+ ],
64
+ };
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from "@storybook/vue3-vite";
2
+ import Login from "./Login.vue";
3
+ import { mockPlatformApiFetch, patchUserManagement } from "@shared/pages/_storybook-helpers";
4
+
5
+ const meta = {
6
+ title: "Shared/Pages/Login",
7
+ component: Login,
8
+ tags: ["autodocs"],
9
+ decorators: [
10
+ () => ({
11
+ setup() {
12
+ mockPlatformApiFetch();
13
+ patchUserManagement();
14
+ },
15
+ template: "<story />",
16
+ }),
17
+ ],
18
+ args: {
19
+ title: "Welcome back",
20
+ subtitle: "Sign in to your account",
21
+ },
22
+ parameters: {
23
+ layout: "fullscreen",
24
+ docs: {
25
+ description: {
26
+ component:
27
+ "Login page with username/password form, SSO provider support, and forgot-password link. SSO providers require a live API and won't appear in Storybook.",
28
+ },
29
+ },
30
+ },
31
+ } satisfies Meta<typeof Login>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {};
37
+
38
+ export const WithCustomBackground: Story = {
39
+ args: {
40
+ background: "https://images.unsplash.com/photo-1557683316-973673baf926?w=1920&q=80",
41
+ title: "Vendor Portal",
42
+ subtitle: "Manage your products and orders",
43
+ },
44
+ };
45
+
46
+ export const SSOOnly: Story = {
47
+ args: {
48
+ ssoOnly: true,
49
+ title: "Enterprise Login",
50
+ subtitle: "Sign in with your organization account",
51
+ },
52
+ };
@@ -0,0 +1,64 @@
1
+ import { onUnmounted } from "vue";
2
+ import type { Meta, StoryObj } from "@storybook/vue3-vite";
3
+ import ResetPassword from "./ResetPassword.vue";
4
+ import { useUserManagement } from "@core/composables/useUserManagement";
5
+ import { mockPlatformApiFetch, patchUserManagement } from "@shared/pages/_storybook-helpers";
6
+
7
+ const meta = {
8
+ title: "Shared/Pages/ResetPassword",
9
+ component: ResetPassword,
10
+ tags: ["autodocs"],
11
+ decorators: [
12
+ () => ({
13
+ setup() {
14
+ mockPlatformApiFetch();
15
+ patchUserManagement();
16
+ },
17
+ template: "<story />",
18
+ }),
19
+ ],
20
+ args: {
21
+ userId: "mock-user-id",
22
+ userName: "john@example.com",
23
+ token: "mock-reset-token",
24
+ },
25
+ parameters: {
26
+ layout: "fullscreen",
27
+ docs: {
28
+ description: {
29
+ component:
30
+ "Reset password page reached via email link. Validates token on mount, then allows setting a new password with confirmation. Auto-signs in on success.",
31
+ },
32
+ },
33
+ },
34
+ } satisfies Meta<typeof ResetPassword>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ export const Default: Story = {};
40
+
41
+ export const WithCustomBackground: Story = {
42
+ args: {
43
+ background: "https://images.unsplash.com/photo-1557683316-973673baf926?w=1920&q=80",
44
+ },
45
+ };
46
+
47
+ export const InvalidToken: Story = {
48
+ args: {
49
+ token: "expired-token",
50
+ },
51
+ decorators: [
52
+ () => ({
53
+ setup() {
54
+ const userManagement = useUserManagement();
55
+ const orig = userManagement.validateToken;
56
+ userManagement.validateToken = async () => false;
57
+ onUnmounted(() => {
58
+ userManagement.validateToken = orig;
59
+ });
60
+ },
61
+ template: "<story />",
62
+ }),
63
+ ],
64
+ };
@@ -0,0 +1,71 @@
1
+ import { defineComponent, onUnmounted } from "vue";
2
+ import { useRouter } from "vue-router";
3
+ import { useUserManagement } from "@core/composables/useUserManagement";
4
+ import { IdentityResult, SecurityResult, SignInResult } from "@core/api/platform";
5
+
6
+ /**
7
+ * Intercepts window.fetch for platform API endpoints that don't exist in Storybook.
8
+ * Returns safe empty responses so composables like useSettings don't throw.
9
+ */
10
+ export function mockPlatformApiFetch() {
11
+ const originalFetch = window.fetch;
12
+
13
+ window.fetch = async function (input: RequestInfo | URL, init?: RequestInit) {
14
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
15
+
16
+ // useSettings → GET /api/platform/settings/ui/customization
17
+ if (url.includes("/api/platform/settings/ui/customization")) {
18
+ return new Response(JSON.stringify({ defaultValue: null }), {
19
+ status: 200,
20
+ headers: { "Content-Type": "application/json" },
21
+ });
22
+ }
23
+
24
+ return originalFetch.call(window, input, init);
25
+ } as typeof fetch;
26
+
27
+ onUnmounted(() => {
28
+ window.fetch = originalFetch;
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Patches useUserManagement methods with safe no-op/success stubs
34
+ * and ensures auth-related routes exist for router.push() calls.
35
+ */
36
+ export function patchUserManagement() {
37
+ const userManagement = useUserManagement();
38
+ const router = useRouter();
39
+
40
+ const originals = {
41
+ signIn: userManagement.signIn,
42
+ signOut: userManagement.signOut,
43
+ validateToken: userManagement.validateToken,
44
+ validatePassword: userManagement.validatePassword,
45
+ resetPasswordByToken: userManagement.resetPasswordByToken,
46
+ requestPasswordReset: userManagement.requestPasswordReset,
47
+ changeUserPassword: userManagement.changeUserPassword,
48
+ };
49
+
50
+ userManagement.signIn = async () => new SignInResult({ succeeded: true });
51
+ userManagement.signOut = async () => {};
52
+ userManagement.validateToken = async () => true;
53
+ userManagement.validatePassword = async () => new IdentityResult({ succeeded: true, errors: [] });
54
+ userManagement.resetPasswordByToken = async () => new SecurityResult({ succeeded: true, errors: [] });
55
+ userManagement.requestPasswordReset = async () => ({ succeeded: true });
56
+ userManagement.changeUserPassword = async () => new SecurityResult({ succeeded: true, errors: [] });
57
+
58
+ for (const name of ["Login", "ForgotPassword", "ChangePassword"] as const) {
59
+ if (!router.hasRoute(name)) {
60
+ router.addRoute({
61
+ name,
62
+ path: `/${name.toLowerCase()}`,
63
+ component: defineComponent({ template: "<div />" }),
64
+ });
65
+ }
66
+ }
67
+
68
+ onUnmounted(() => {
69
+ Object.assign(userManagement, originals);
70
+ });
71
+ }
@@ -767,7 +767,7 @@ watch(
767
767
  }
768
768
 
769
769
  &__no-options-text {
770
- @apply tw-m-4 tw-text-lg tw-font-medium;
770
+ @apply tw-m-4 tw-text-sm tw-font-normal tw-text-[color:var(--select-placeholder-color)];
771
771
  }
772
772
 
773
773
  &__option {
@@ -136,8 +136,7 @@ const isItemActive = (url?: string): boolean => {
136
136
  --app-menu-close-color: var(--app-menu-burger-color, var(--primary-500));
137
137
  --app-menu-burger-color: var(--primary-500);
138
138
 
139
- --app-backdrop-overlay-bg: var(--additional-50);
140
- --app-backdrop-overlay: rgb(from var(--app-backdrop-overlay-bg) r g b / 75%);
139
+ --app-backdrop-overlay: var(--overlay-bg);
141
140
 
142
141
  --app-backdrop-shadow-color: var(--additional-950);
143
142
  --app-backdrop-shadow:
@@ -167,12 +167,9 @@ function handleItemClick(item: IBladeToolbar) {
167
167
  --blade-toolbar-mobile-pill-bg-color: var(--primary-500);
168
168
  --blade-toolbar-mobile-toggle-border-color: var(--primary-200);
169
169
  --blade-toolbar-mobile-toggle-icon-color: var(--additional-50);
170
- --blade-toolbar-mobile-overlay-bg-color: var(--additional-950);
171
- --blade-toolbar-mobile-action-bg: var(--additional-50);
170
+ --blade-toolbar-mobile-action-bg: var(--surface-color);
172
171
  --blade-toolbar-mobile-action-text: var(--neutrals-800);
173
- --blade-toolbar-mobile-backdrop-blur: 12px;
174
- // Circle button colors (same as ToolbarCircleButton — must be defined here
175
- // because ToolbarCircleButton doesn't mount on mobile)
172
+ // Circle button colors
176
173
  --blade-toolbar-circle-button-text-color: var(--additional-50);
177
174
  --blade-toolbar-circle-button-bg-color: var(--neutrals-500);
178
175
  --blade-toolbar-circle-button-main-bg-color: var(--primary-500);
@@ -189,9 +186,9 @@ $touch-min: 44px;
189
186
  // ── Backdrop ──────────────────────────────────────────────────────────
190
187
  &__backdrop {
191
188
  @apply tw-fixed tw-inset-0 tw-z-[55];
192
- background: rgba(0, 0, 0, 0.4);
193
- backdrop-filter: blur(var(--blade-toolbar-mobile-backdrop-blur));
194
- -webkit-backdrop-filter: blur(var(--blade-toolbar-mobile-backdrop-blur));
189
+ background: var(--overlay-bg);
190
+ backdrop-filter: blur(var(--overlay-blur));
191
+ -webkit-backdrop-filter: blur(var(--overlay-blur));
195
192
  }
196
193
 
197
194
  // ── Menu container ────────────────────────────────────────────────────
@@ -223,21 +220,25 @@ $touch-min: 44px;
223
220
  gap: 12px;
224
221
  padding: 4px 4px 4px 16px;
225
222
  min-height: $touch-min;
226
- border: none;
223
+ border: 1px solid var(--surface-border);
227
224
  border-radius: 28px;
228
- background: var(--blade-toolbar-mobile-action-bg);
229
- box-shadow:
230
- 0 2px 8px rgba(0, 0, 0, 0.1),
231
- 0 1px 3px rgba(0, 0, 0, 0.06);
225
+ background: color-mix(in srgb, var(--blade-toolbar-mobile-action-bg) 92%, transparent);
226
+ backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur));
227
+ -webkit-backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur));
228
+ box-shadow: var(--shadow-sm);
232
229
  cursor: pointer;
233
230
 
234
- // Initial state (hidden)
235
231
  opacity: 0;
236
232
  transform: translateY(16px);
237
233
  transition:
238
234
  opacity 0.28s $spring,
239
235
  transform 0.28s $spring;
240
- // transition-delay set via inline style
236
+
237
+ &:hover:not(:disabled) {
238
+ background: var(--blade-toolbar-mobile-action-bg);
239
+ box-shadow: var(--shadow-md);
240
+ transition-duration: 0.15s;
241
+ }
241
242
 
242
243
  &:active:not(:disabled) {
243
244
  transform: scale(0.96) !important;
@@ -249,7 +250,6 @@ $touch-min: 44px;
249
250
  cursor: default;
250
251
  }
251
252
 
252
- // Focus-visible ring for keyboard navigation
253
253
  &:focus-visible {
254
254
  outline: 2px solid var(--blade-toolbar-mobile-pill-bg-color);
255
255
  outline-offset: 2px;
@@ -291,9 +291,7 @@ $touch-min: 44px;
291
291
  color: var(--blade-toolbar-mobile-toggle-icon-color);
292
292
  cursor: pointer;
293
293
  -webkit-tap-highlight-color: transparent;
294
- box-shadow:
295
- 0 4px 12px rgba(0, 0, 0, 0.16),
296
- 0 1px 4px rgba(0, 0, 0, 0.08);
294
+ box-shadow: var(--shadow-sm);
297
295
 
298
296
  &:active {
299
297
  transform: scale(0.92);
@@ -321,9 +319,7 @@ $touch-min: 44px;
321
319
  border-radius: calc(#{$touch-min} / 2);
322
320
  background: var(--blade-toolbar-mobile-pill-bg-color);
323
321
  overflow: hidden;
324
- box-shadow:
325
- 0 4px 12px rgba(0, 0, 0, 0.16),
326
- 0 1px 4px rgba(0, 0, 0, 0.08);
322
+ box-shadow: var(--shadow-sm);
327
323
  }
328
324
 
329
325
  &__pill-action {
@@ -234,8 +234,8 @@ function handleDialogDismiss(): void {
234
234
  <style lang="scss">
235
235
  :root {
236
236
  --vc-popup-border-radius: var(--popup-border-radius, 6px);
237
- --vc-popup-shadow: var(--popup-shadow, 0 24px 48px rgb(15 23 42 / 0.2));
238
- --vc-popup-overlay-blur: var(--popup-overlay-blur, 3px);
237
+ --vc-popup-shadow: var(--popup-shadow, var(--shadow-md));
238
+ --vc-popup-overlay-blur: var(--popup-overlay-blur, var(--overlay-blur));
239
239
 
240
240
  // Deprecated aliases (prefer --vc-popup-* variables for new themes)
241
241
  --popup-close-btn-bg: var(--neutrals-100);
@@ -247,8 +247,7 @@ function handleDialogDismiss(): void {
247
247
  --popup-success-icon-color: var(--success-500);
248
248
  --popup-info-icon-color: var(--info-500);
249
249
  --popup-footer-separator: var(--neutrals-200);
250
- --popup-overlay-color: var(--additional-50);
251
- --popup-overlay: rgb(from var(--popup-overlay-color) r g b / 75%);
250
+ --popup-overlay: var(--popup-overlay-override, var(--overlay-bg));
252
251
  --popup-bg: var(--additional-50);
253
252
  }
254
253
 
@@ -563,10 +563,10 @@ defineExpose({
563
563
  --vc-sidebar-bg-elevated: color-mix(in srgb, var(--additional-50) 90%, white 10%);
564
564
  --vc-sidebar-title-color: var(--additional-950);
565
565
  --vc-sidebar-subtitle-color: var(--neutrals-600);
566
- --vc-sidebar-overlay-color: rgb(15 23 42 / 0.38);
566
+ --vc-sidebar-overlay-color: var(--overlay-bg);
567
567
  --vc-sidebar-close-color: var(--neutrals-600);
568
- --vc-sidebar-border-color: color-mix(in srgb, var(--neutrals-300) 72%, white 28%);
569
- --vc-sidebar-shadow: 0 24px 48px rgb(15 23 42 / 0.16), 0 8px 20px rgb(15 23 42 / 0.1);
568
+ --vc-sidebar-border-color: var(--surface-border);
569
+ --vc-sidebar-shadow: var(--shadow-md);
570
570
  --vc-sidebar-radius: 16px;
571
571
  --vc-sidebar-inset-gap: 12px;
572
572
  --vc-sidebar-header-height: 72px;
@@ -575,7 +575,9 @@ defineExpose({
575
575
  @apply tw-fixed tw-inset-0 tw-pointer-events-none;
576
576
 
577
577
  &__overlay {
578
- @apply tw-absolute tw-inset-0 tw-backdrop-blur-[4px] tw-pointer-events-auto;
578
+ @apply tw-absolute tw-inset-0 tw-pointer-events-auto;
579
+ backdrop-filter: blur(var(--overlay-blur));
580
+ -webkit-backdrop-filter: blur(var(--overlay-blur));
579
581
  background: var(--vc-sidebar-overlay-color);
580
582
  }
581
583
 
@@ -616,13 +618,11 @@ defineExpose({
616
618
 
617
619
  &--elevated {
618
620
  background: var(--vc-sidebar-bg-elevated);
619
- box-shadow:
620
- 0 28px 64px rgb(15 23 42 / 0.22),
621
- 0 10px 24px rgb(15 23 42 / 0.16);
621
+ box-shadow: var(--shadow-lg);
622
622
  }
623
623
 
624
624
  &--minimal {
625
- box-shadow: 0 12px 28px rgb(15 23 42 / 0.1);
625
+ box-shadow: var(--shadow-sm);
626
626
  border-color: color-mix(in srgb, var(--neutrals-200) 86%, white 14%);
627
627
  }
628
628
 
@@ -668,7 +668,8 @@ defineExpose({
668
668
  @apply tw-border-b-[color:var(--vc-sidebar-border-color)] tw-sticky tw-top-0 tw-z-[1];
669
669
  min-height: var(--vc-sidebar-header-height);
670
670
  background: inherit;
671
- backdrop-filter: saturate(140%) blur(6px);
671
+ backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur));
672
+ -webkit-backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur));
672
673
  }
673
674
 
674
675
  &__title-wrap {
@@ -725,7 +726,8 @@ defineExpose({
725
726
  @apply tw-mt-auto tw-sticky tw-bottom-0 tw-z-[1] tw-border-t tw-border-solid tw-border-t-[color:var(--vc-sidebar-border-color)];
726
727
  min-height: var(--vc-sidebar-footer-height);
727
728
  background: inherit;
728
- backdrop-filter: saturate(140%) blur(6px);
729
+ backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur));
730
+ -webkit-backdrop-filter: saturate(var(--glass-saturate)) blur(var(--glass-blur));
729
731
  }
730
732
  }
731
733
 
@@ -139,8 +139,11 @@
139
139
  :get-column-width="cols.getEffectiveColumnWidth"
140
140
  :get-cell-style="cols.getCellStyle"
141
141
  :show-selection-cell="hasSelectionColumn && !isSelectionViaColumn"
142
- :empty-title="emptyTitle"
143
- :empty-description="emptyDescription"
142
+ :empty-icon="resolvedEmptyIcon"
143
+ :empty-title="resolvedEmptyTitle"
144
+ :empty-description="resolvedEmptyDescription"
145
+ :empty-action-label="resolvedEmptyActionLabel"
146
+ :empty-action-handler="resolvedEmptyActionHandler"
144
147
  :loading-text="loadingText"
145
148
  :grouping-enabled="rowGrouping.isGroupingEnabled.value"
146
149
  :grouped-data="rowGrouping.groupedData.value"
@@ -213,10 +216,11 @@
213
216
  />
214
217
  </template>
215
218
  <template
216
- v-if="$slots.empty"
219
+ v-if="$slots['not-found'] || $slots.empty"
217
220
  #empty
218
221
  >
219
- <slot name="empty" />
222
+ <slot v-if="isNotFoundState && $slots['not-found']" name="not-found" />
223
+ <slot v-else name="empty" />
220
224
  </template>
221
225
  <template
222
226
  v-if="$slots.loading"
@@ -283,12 +287,16 @@
283
287
  @refresh="emit('pull-refresh')"
284
288
  >
285
289
  <template #empty>
286
- <slot name="empty">
287
- <TableEmpty
288
- :title="emptyTitle"
289
- :description="emptyDescription"
290
- />
291
- </slot>
290
+ <slot v-if="isNotFoundState && $slots['not-found']" name="not-found" />
291
+ <slot v-else-if="$slots.empty" name="empty" />
292
+ <TableEmpty
293
+ v-else
294
+ :icon="resolvedEmptyIcon"
295
+ :title="resolvedEmptyTitle"
296
+ :description="resolvedEmptyDescription"
297
+ :action-label="resolvedEmptyActionLabel"
298
+ :action-handler="resolvedEmptyActionHandler"
299
+ />
292
300
  </template>
293
301
  </DataTableMobileView>
294
302
  </div>
@@ -367,7 +375,7 @@ import {
367
375
  import { ColumnCollector, type ColumnInstance } from "@ui/components/organisms/vc-table/utils/ColumnCollector";
368
376
  import { ColumnCollectorKey, FilterContextKey, HasFlexColumnsKey, IsColumnReorderingKey } from "@ui/components/organisms/vc-table/keys";
369
377
  import { IsMobileKey } from "@framework/injection-keys";
370
- import type { VcColumnProps, VcDataTableExtendedProps, FilterValue, EditChange, TableAction, SortMeta, MobileSwipeAction } from "@ui/components/organisms/vc-table/types";
378
+ import type { VcColumnProps, VcDataTableExtendedProps, FilterValue, EditChange, TableAction, SortMeta, MobileSwipeAction, TableStateConfig } from "@ui/components/organisms/vc-table/types";
371
379
  import type { DataTablePersistedState } from "@ui/components/organisms/vc-table/composables";
372
380
 
373
381
  const props = withDefaults(defineProps<VcDataTableExtendedProps<T>>(), {
@@ -420,7 +428,10 @@ const props = withDefaults(defineProps<VcDataTableExtendedProps<T>>(), {
420
428
  searchable: false,
421
429
  searchValue: undefined,
422
430
  searchPlaceholder: "Search...",
431
+ emptyState: undefined,
432
+ notFoundState: undefined,
423
433
  searchDebounce: 300,
434
+ activeItemId: undefined,
424
435
  });
425
436
 
426
437
  // Emits
@@ -484,6 +495,8 @@ const emit = defineEmits<{
484
495
  filter: [event: { filters: Record<string, unknown>; filteredValue: T[] }];
485
496
 
486
497
  // === Row Interactions ===
498
+ /** v-model update for active (highlighted) row ID */
499
+ "update:activeItemId": [value: string | undefined];
487
500
  /** Emitted when a row is clicked */
488
501
  "row-click": [event: { data: T; index: number; originalEvent: Event }];
489
502
  /** Emitted when a row action button/menu item is activated */
@@ -557,6 +570,8 @@ defineSlots<{
557
570
  }) => VNode[];
558
571
  /** Custom content for expanded rows */
559
572
  expansion?: (props: { data: T; index: number }) => VNode[];
573
+ /** Custom not-found state content (shown when search yields no results) */
574
+ "not-found"?: () => VNode[];
560
575
  /** Custom empty state content */
561
576
  empty?: () => VNode[];
562
577
  /** Custom loading state content */
@@ -579,6 +594,34 @@ const { t } = useI18n({ useScope: "global" });
579
594
 
580
595
  const emptyTitle = computed(() => t("COMPONENTS.ORGANISMS.VC_TABLE.EMPTY_TITLE"));
581
596
  const emptyDescription = computed(() => t("COMPONENTS.ORGANISMS.VC_TABLE.EMPTY_DESCRIPTION"));
597
+ const notFoundTitle = computed(() => t("COMPONENTS.ORGANISMS.VC_TABLE.NOT_FOUND_TITLE"));
598
+ const notFoundDescription = computed(() => t("COMPONENTS.ORGANISMS.VC_TABLE.NOT_FOUND_DESCRIPTION"));
599
+
600
+ /** Detect not-found state: items empty + active search or filters */
601
+ const isNotFoundState = computed(
602
+ () => displayItems.value.length === 0 && !props.loading && (internalSearchValue.value !== "" || hasActiveFilters.value),
603
+ );
604
+
605
+ /** Resolved title/description/icon/action for the current empty-like state */
606
+ const resolvedEmptyIcon = computed(() =>
607
+ isNotFoundState.value ? props.notFoundState?.icon : props.emptyState?.icon,
608
+ );
609
+ const resolvedEmptyTitle = computed(() =>
610
+ isNotFoundState.value
611
+ ? (props.notFoundState?.title ?? notFoundTitle.value)
612
+ : (props.emptyState?.title ?? emptyTitle.value),
613
+ );
614
+ const resolvedEmptyDescription = computed(() =>
615
+ isNotFoundState.value
616
+ ? (props.notFoundState?.description ?? notFoundDescription.value)
617
+ : (props.emptyState?.description ?? emptyDescription.value),
618
+ );
619
+ const resolvedEmptyActionLabel = computed(() =>
620
+ isNotFoundState.value ? props.notFoundState?.actionLabel : props.emptyState?.actionLabel,
621
+ );
622
+ const resolvedEmptyActionHandler = computed(() =>
623
+ isNotFoundState.value ? props.notFoundState?.actionHandler : props.emptyState?.actionHandler,
624
+ );
582
625
  const loadingText = computed(() => t("COMPONENTS.ORGANISMS.VC_TABLE.LOADING"));
583
626
 
584
627
  /** Track whether data has ever been loaded — distinguishes initial load from refresh */
@@ -1412,6 +1455,7 @@ const getRowProps = (item: T, index: number) => ({
1412
1455
 
1413
1456
  // Selection
1414
1457
  isSelected: selection.isSelected(item),
1458
+ isActive: props.activeItemId != null && getItemKey(item, index) === String(props.activeItemId),
1415
1459
  isSelectable: selection.canSelect(item),
1416
1460
  selectionMode: effectiveSelectionMode.value,
1417
1461
  showSelectionCell: hasSelectionColumn.value && !isSelectionViaColumn.value,
@@ -1494,6 +1538,9 @@ const handleRowSelectionChange = (item: T, eventOrValue?: Event | boolean) => {
1494
1538
  };
1495
1539
 
1496
1540
  const handleRowClick = (item: T, index: number, event: Event) => {
1541
+ const itemKey = getItemKey(item, index);
1542
+ const isSameItem = props.activeItemId != null && itemKey === String(props.activeItemId);
1543
+ emit("update:activeItemId", isSameItem ? undefined : itemKey);
1497
1544
  emit("row-click", { data: item, index, originalEvent: event });
1498
1545
  const target = event.target as HTMLElement;
1499
1546
  const isCheckboxClick = target.tagName === "INPUT" && target.getAttribute("type") === "checkbox";