diff --git a/apps/app/.eslintrc.cjs b/apps/app/.eslintrc.cjs index 5d74d343..cc1069d7 100644 --- a/apps/app/.eslintrc.cjs +++ b/apps/app/.eslintrc.cjs @@ -252,9 +252,16 @@ module.exports = { // (components-foundation) but NOT any other v1 component zone. // No v1 `from` rule lists components-v2/pages-v2 → back-porting // is structurally impossible (RFC-WS-GUI-REDESIGN AD-G5). + // layouts-v2 (src/layouts/*V2*.vue, e.g. OrganizerLayoutV2) is + // the v2 shell-composition zone: SAME v2 capability as pages-v2 + // (may import components-v2 + navigation) so AD-G2's + // "OrganizerLayoutV2 wraps AppShellV2" holds, WITHOUT widening + // the v1 `layouts` zone (still cannot reach components-v2 → + // AD-G5 isolation intact). Locked by tests/unit/boundaries-v2.spec.ts. { from: 'components-foundation', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-foundation'] }, { from: 'components-v2', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-v2', 'components-foundation'] }, { from: 'pages-v2', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components-v2', 'components-foundation', 'layouts', 'plugins'] }, + { from: 'layouts-v2', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components-v2', 'components-foundation', 'layouts', 'plugins'] }, { from: 'components', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components', 'components-shared', 'components-organizer'] }, { from: 'layouts', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components', 'components-shared', 'components-portal', 'components-organizer', 'layouts'] }, @@ -306,9 +313,23 @@ module.exports = { // same `type` so both src/components/forms/** and src/components/Icon.vue // are captured before the generic `components` catch-all. { type: 'components-foundation', pattern: 'src/components/forms/**' }, - { type: 'components-foundation', pattern: 'src/components/Icon.vue' }, + // mode:'file' is REQUIRED for a single-file pattern. Without it + // eslint-plugin-boundaries matches in the default 'folder' mode, + // so 'src/components/Icon.vue' never matches and Icon.vue falls + // through to the generic `components` catch-all below — breaking + // the sanctioned components-v2 → Icon bridge (RFC AD-G5). The + // forms/** entry above is a folder glob so it is unaffected. + { type: 'components-foundation', pattern: 'src/components/Icon.vue', mode: 'file' }, { type: 'components-v2', pattern: 'src/components-v2/**' }, { type: 'components', pattern: 'src/components/**' }, + // layouts-v2 MUST precede the generic `layouts` element: first + // match wins. The single `*` does not cross `/`, so this matches + // only top-level v2 layout files (src/layouts/OrganizerLayoutV2.vue) + // and NOT src/layouts/components/AppShellV2.vue (subdir → stays + // `layouts`, which is correct: AppShellV2 imports only stores). + // mode:'file' is REQUIRED for a file-glob element (same reason as + // the Icon.vue bridge above) — RFC AD-G5 / boundaries-v2.spec.ts. + { type: 'layouts-v2', pattern: 'src/layouts/*V2*.vue', mode: 'file' }, { type: 'layouts', pattern: 'src/layouts/**' }, { type: 'pages-register', pattern: 'src/pages/register/**' }, { type: 'pages-portal', pattern: 'src/pages/portal/**' }, diff --git a/apps/app/.storybook/preview.ts b/apps/app/.storybook/preview.ts index ad43da28..a263f5cd 100644 --- a/apps/app/.storybook/preview.ts +++ b/apps/app/.storybook/preview.ts @@ -1,28 +1,41 @@ import type { Preview } from '@storybook/vue3-vite' import { setup } from '@storybook/vue3-vite' - +import { createMemoryHistory, createRouter } from 'vue-router' import { installPrimeVue } from '../src/plugins/primevue' +// Side-effect: bootstrap the Tabler set so @iconify/vue renders +// real SVG in Storybook (mirrors main.ts; preview.ts is the SB entry). +import '../src/plugins/iconify' + import '../src/assets/styles/tailwind.css' +const noop = { template: '
' } +const routeNames = [ + 'dashboard', 'events', 'organisation', 'members', + 'organisation-companies', 'organisation-form-failures', + 'organisation-settings', 'platform', 'platform-organisations', + 'platform-users', 'platform-form-failures', 'platform-activity-log', +] + +export const storyRouter = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', name: 'home', component: noop }, + ...routeNames.map(name => ({ path: `/${name}`, name, component: noop })), + { path: '/:pathMatch(.*)*', name: 'catchall', component: noop }, + ], +}) + setup((app) => { installPrimeVue(app) + app.use(storyRouter) }) const preview: Preview = { parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - docs: { - toc: true, - }, - a11y: { - test: 'todo', - }, + controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } }, + docs: { toc: true }, + a11y: { test: 'todo' }, }, } diff --git a/apps/app/auto-imports.d.ts b/apps/app/auto-imports.d.ts index dbef1523..e9a18b71 100644 --- a/apps/app/auto-imports.d.ts +++ b/apps/app/auto-imports.d.ts @@ -136,10 +136,12 @@ declare global { const refThrottled: typeof import('@vueuse/core')['refThrottled'] const refWithControl: typeof import('@vueuse/core')['refWithControl'] const regexValidator: typeof import('./src/@core/utils/validators')['regexValidator'] + const registerDrawerComponent: typeof import('./src/composables/drawerRegistry')['registerDrawerComponent'] const registerPlugins: typeof import('./src/@core/utils/plugins')['registerPlugins'] const registerPlugins_: typeof import('./src/@core/utils/plugins')['registerPlugins_'] const requiredValidator: typeof import('./src/@core/utils/validators')['requiredValidator'] const resolveComponent: typeof import('vue')['resolveComponent'] + const resolveDrawerComponent: typeof import('./src/composables/drawerRegistry')['resolveDrawerComponent'] const resolvePostLoginTarget: typeof import('./src/utils/postLoginRedirect')['resolvePostLoginTarget'] const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] @@ -156,10 +158,12 @@ declare global { const templateRef: typeof import('@vueuse/core')['templateRef'] const throttledRef: typeof import('@vueuse/core')['throttledRef'] const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] + const toBreadcrumbItems: typeof import('./src/composables/useBreadcrumb')['toBreadcrumbItems'] const toRaw: typeof import('vue')['toRaw'] const toReactive: typeof import('@vueuse/core')['toReactive'] const toRef: typeof import('vue')['toRef'] const toRefs: typeof import('vue')['toRefs'] + const toV2NavGroups: typeof import('./src/composables/useV2Nav')['toV2NavGroups'] const toValue: typeof import('vue')['toValue'] const triggerRef: typeof import('vue')['triggerRef'] const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] @@ -195,6 +199,7 @@ declare global { const useBase64: typeof import('@vueuse/core')['useBase64'] const useBattery: typeof import('@vueuse/core')['useBattery'] const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] + const useBreadcrumb: typeof import('./src/composables/useBreadcrumb')['useBreadcrumb'] const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] @@ -353,6 +358,7 @@ declare global { const useTrunc: typeof import('@vueuse/math')['useTrunc'] const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] + const useV2Nav: typeof import('./src/composables/useV2Nav')['useV2Nav'] const useVModel: typeof import('@vueuse/core')['useVModel'] const useVModels: typeof import('@vueuse/core')['useVModels'] const useVibrate: typeof import('@vueuse/core')['useVibrate'] @@ -521,9 +527,11 @@ declare module 'vue' { readonly refThrottled: UnwrapRef readonly refWithControl: UnwrapRef readonly regexValidator: UnwrapRef + readonly registerDrawerComponent: UnwrapRef readonly registerPlugins: UnwrapRef readonly requiredValidator: UnwrapRef readonly resolveComponent: UnwrapRef + readonly resolveDrawerComponent: UnwrapRef readonly resolvePostLoginTarget: UnwrapRef readonly resolveRef: UnwrapRef readonly resolveUnref: UnwrapRef @@ -540,10 +548,12 @@ declare module 'vue' { readonly templateRef: UnwrapRef readonly throttledRef: UnwrapRef readonly throttledWatch: UnwrapRef + readonly toBreadcrumbItems: UnwrapRef readonly toRaw: UnwrapRef readonly toReactive: UnwrapRef readonly toRef: UnwrapRef readonly toRefs: UnwrapRef + readonly toV2NavGroups: UnwrapRef readonly toValue: UnwrapRef readonly triggerRef: UnwrapRef readonly tryOnBeforeMount: UnwrapRef @@ -577,6 +587,7 @@ declare module 'vue' { readonly useBase64: UnwrapRef readonly useBattery: UnwrapRef readonly useBluetooth: UnwrapRef + readonly useBreadcrumb: UnwrapRef readonly useBreakpoints: UnwrapRef readonly useBroadcastChannel: UnwrapRef readonly useBrowserLocation: UnwrapRef @@ -729,6 +740,7 @@ declare module 'vue' { readonly useTrunc: UnwrapRef readonly useUrlSearchParams: UnwrapRef readonly useUserMedia: UnwrapRef + readonly useV2Nav: UnwrapRef readonly useVModel: UnwrapRef readonly useVModels: UnwrapRef readonly useVibrate: UnwrapRef diff --git a/apps/app/src/components-v2/layout/AppSidebar.vue b/apps/app/src/components-v2/layout/AppSidebar.vue new file mode 100644 index 00000000..171f3837 --- /dev/null +++ b/apps/app/src/components-v2/layout/AppSidebar.vue @@ -0,0 +1,128 @@ + + + diff --git a/apps/app/src/components-v2/layout/AppTopbar.vue b/apps/app/src/components-v2/layout/AppTopbar.vue new file mode 100644 index 00000000..4da4dc39 --- /dev/null +++ b/apps/app/src/components-v2/layout/AppTopbar.vue @@ -0,0 +1,464 @@ + + + + + diff --git a/apps/app/src/components-v2/layout/RightDrawer.vue b/apps/app/src/components-v2/layout/RightDrawer.vue new file mode 100644 index 00000000..aa6379e7 --- /dev/null +++ b/apps/app/src/components-v2/layout/RightDrawer.vue @@ -0,0 +1,170 @@ + + + diff --git a/apps/app/src/components-v2/layout/SidebarHeader.vue b/apps/app/src/components-v2/layout/SidebarHeader.vue new file mode 100644 index 00000000..1ffeaec5 --- /dev/null +++ b/apps/app/src/components-v2/layout/SidebarHeader.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/apps/app/src/components-v2/layout/SidebarNav.vue b/apps/app/src/components-v2/layout/SidebarNav.vue new file mode 100644 index 00000000..7fac0a39 --- /dev/null +++ b/apps/app/src/components-v2/layout/SidebarNav.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue b/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue new file mode 100644 index 00000000..c69cefd3 --- /dev/null +++ b/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/apps/app/src/components-v2/layout/__tests__/AppSidebar.spec.ts b/apps/app/src/components-v2/layout/__tests__/AppSidebar.spec.ts new file mode 100644 index 00000000..32cce9b2 --- /dev/null +++ b/apps/app/src/components-v2/layout/__tests__/AppSidebar.spec.ts @@ -0,0 +1,258 @@ +/** + * AppSidebar.spec.ts — unit tests for AppSidebar composition and mobile wiring. + * + * Strategy: mount with @vue/test-utils stubs for all heavy children (SidebarHeader, + * SidebarNav, WorkspaceSwitcher, Drawer) so we test only: + * 1. Renders the 3 child components (SidebarHeader, SidebarNav, WorkspaceSwitcher). + * 2. Passes `groups` prop to SidebarNav. + * 3. Mobile Drawer v-model:visible wires to shell.mobileOpen (get path). + * 4. Drawer close (v-model:visible = false) calls shell.setMobileOpen(false). + * 5. Drawer is NOT rendered when isMobile=false (desktop); IS rendered when isMobile=true. + * 6. Desktop