@flux-ui/application 3.0.0-next.39
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/README.md +16 -0
- package/dist/component/FluxApplication.vue.d.ts +28 -0
- package/dist/component/FluxApplicationContent.vue.d.ts +24 -0
- package/dist/component/FluxApplicationHero.vue.d.ts +26 -0
- package/dist/component/FluxApplicationMenu.vue.d.ts +29 -0
- package/dist/component/FluxApplicationMenuAccount.vue.d.ts +29 -0
- package/dist/component/FluxApplicationMenuContext.vue.d.ts +15 -0
- package/dist/component/FluxApplicationMenuContextStack.vue.d.ts +6 -0
- package/dist/component/FluxApplicationMenuPromo.vue.d.ts +22 -0
- package/dist/component/FluxApplicationMenuToggle.vue.d.ts +3 -0
- package/dist/component/FluxApplicationSection.vue.d.ts +26 -0
- package/dist/component/FluxApplicationSide.vue.d.ts +20 -0
- package/dist/component/FluxApplicationTop.vue.d.ts +29 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/composable/index.d.ts +3 -0
- package/dist/composable/useApplicationContextMenu.d.ts +7 -0
- package/dist/composable/useApplicationContextRegistration.d.ts +2 -0
- package/dist/composable/useApplicationInjection.d.ts +2 -0
- package/dist/composable/useApplicationMenu.d.ts +26 -0
- package/dist/data/index.d.ts +29 -0
- package/dist/index.css +746 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4331 -0
- package/dist/index.js.map +1 -0
- package/dist/routing/useNamedRoutes.d.ts +14 -0
- package/dist/routing/useRoute.d.ts +8 -0
- package/dist/types.d.ts +13 -0
- package/package.json +71 -0
- package/src/component/FluxApplication.vue +144 -0
- package/src/component/FluxApplicationContent.vue +37 -0
- package/src/component/FluxApplicationHero.vue +34 -0
- package/src/component/FluxApplicationMenu.vue +73 -0
- package/src/component/FluxApplicationMenuAccount.vue +47 -0
- package/src/component/FluxApplicationMenuContext.vue +78 -0
- package/src/component/FluxApplicationMenuContextStack.vue +53 -0
- package/src/component/FluxApplicationMenuPromo.vue +23 -0
- package/src/component/FluxApplicationMenuToggle.vue +30 -0
- package/src/component/FluxApplicationSection.vue +40 -0
- package/src/component/FluxApplicationSide.vue +16 -0
- package/src/component/FluxApplicationTop.vue +74 -0
- package/src/component/index.ts +12 -0
- package/src/composable/index.ts +3 -0
- package/src/composable/useApplicationContextMenu.ts +19 -0
- package/src/composable/useApplicationContextRegistration.ts +25 -0
- package/src/composable/useApplicationInjection.ts +12 -0
- package/src/composable/useApplicationMenu.ts +79 -0
- package/src/css/component/Application.module.scss +88 -0
- package/src/css/component/ApplicationContent.module.scss +119 -0
- package/src/css/component/ApplicationHero.module.scss +17 -0
- package/src/css/component/ApplicationMenu.module.scss +421 -0
- package/src/css/component/ApplicationSection.module.scss +43 -0
- package/src/css/component/ApplicationSide.module.scss +21 -0
- package/src/css/component/ApplicationTop.module.scss +111 -0
- package/src/data/index.ts +38 -0
- package/src/index.ts +3 -0
- package/src/routing/useNamedRoutes.ts +34 -0
- package/src/routing/useRoute.ts +11 -0
- package/src/types.d.ts +13 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComputedRef, Ref } from 'vue';
|
|
2
|
+
import { RouteRecordNormalized } from 'vue-router';
|
|
3
|
+
/**
|
|
4
|
+
* Returns **all** matched route records that expose a named view with
|
|
5
|
+
* the given `name`, paired with their depth in `route.matched`. Powers
|
|
6
|
+
* `<FluxApplicationMenuContextStack>` so every level of a nested route
|
|
7
|
+
* tree can render its own menu component (instead of vue-router's
|
|
8
|
+
* default behaviour of only rendering the deepest one).
|
|
9
|
+
*/
|
|
10
|
+
export default function (nameRef: Ref<string> | string): ComputedRef<NamedRouteMatch[]>;
|
|
11
|
+
export type NamedRouteMatch = {
|
|
12
|
+
readonly depth: number;
|
|
13
|
+
readonly record: RouteRecordNormalized;
|
|
14
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RouteLocationNormalizedLoaded } from 'vue-router';
|
|
2
|
+
/**
|
|
3
|
+
* Internal alias for vue-router's `useRoute`. Exists so the rest of the
|
|
4
|
+
* package can import a single, controlled `useRoute` symbol — making it
|
|
5
|
+
* trivial to swap in a wrapper later (e.g. modal-aware variants) without
|
|
6
|
+
* touching every call site.
|
|
7
|
+
*/
|
|
8
|
+
export default function (): RouteLocationNormalizedLoaded;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare module '*.module.css' {
|
|
2
|
+
const content: Record<string, string>;
|
|
3
|
+
export default content;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module '*.module.scss' {
|
|
7
|
+
const content: Record<string, string>;
|
|
8
|
+
export default content;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '*.svg' {
|
|
12
|
+
export = string;
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flux-ui/application",
|
|
3
|
+
"description": "Contains components to create applications with Flux UI.",
|
|
4
|
+
"version": "3.0.0-next.39",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"funding": "https://github.com/sponsors/basmilius",
|
|
8
|
+
"homepage": "https://flux-ui.dev",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/basmilius/flux.git",
|
|
12
|
+
"directory": "packages/application"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ui library",
|
|
16
|
+
"component library",
|
|
17
|
+
"design system",
|
|
18
|
+
"vue",
|
|
19
|
+
"vue 3",
|
|
20
|
+
"ui",
|
|
21
|
+
"components",
|
|
22
|
+
"flux",
|
|
23
|
+
"application"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "vue-tsc && vite build",
|
|
27
|
+
"dev": "vite build --watch --mode development"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=23"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./style.css": "./dist/index.css"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"src",
|
|
46
|
+
"tsconfig.json"
|
|
47
|
+
],
|
|
48
|
+
"main": "./dist/index.js",
|
|
49
|
+
"module": "./dist/index.js",
|
|
50
|
+
"types": "./dist/index.d.ts",
|
|
51
|
+
"typings": "./dist/index.d.ts",
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@flux-ui/components": "3.0.0-next.39",
|
|
55
|
+
"@flux-ui/internals": "3.0.0-next.39",
|
|
56
|
+
"clsx": "^2.1.1",
|
|
57
|
+
"vue": "^3.6.0-beta.10",
|
|
58
|
+
"vue-router": "^5.0.6"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@basmilius/vite-preset": "^3.19.0",
|
|
62
|
+
"@flux-ui/types": "3.0.0-next.39",
|
|
63
|
+
"@types/node": "^25.6.0",
|
|
64
|
+
"@vitejs/plugin-vue": "^6.0.6",
|
|
65
|
+
"@vue/tsconfig": "^0.9.1",
|
|
66
|
+
"sass-embedded": "^1.99.0",
|
|
67
|
+
"typescript": "^6.0.3",
|
|
68
|
+
"vite": "^8.0.10",
|
|
69
|
+
"vue-tsc": "^3.2.7"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="$style.application">
|
|
3
|
+
<slot name="menu"/>
|
|
4
|
+
|
|
5
|
+
<div :class="$style.applicationBody">
|
|
6
|
+
<slot/>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<slot name="side"/>
|
|
10
|
+
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
aria-label="Close menu"
|
|
14
|
+
:class="$style.applicationMenuBackdrop"
|
|
15
|
+
@click="isMenuCollapsed = true"/>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script
|
|
20
|
+
lang="ts"
|
|
21
|
+
setup>
|
|
22
|
+
import { useRemembered } from '@flux-ui/internals';
|
|
23
|
+
import { computed, onUnmounted, provide, ref, shallowRef, toRef, type VNode, watch } from 'vue';
|
|
24
|
+
import { type FluxApplicationContextInfo, FluxApplicationInjectionKey, type FluxApplicationLayout } from '../data';
|
|
25
|
+
import useNamedRoutes from '../routing/useNamedRoutes';
|
|
26
|
+
import useRoute from '../routing/useRoute';
|
|
27
|
+
import $style from '../css/component/Application.module.scss';
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
contextMenuName = 'menu',
|
|
31
|
+
showDesktopMenuToggle = false
|
|
32
|
+
} = defineProps<{
|
|
33
|
+
readonly contextMenuName?: string;
|
|
34
|
+
readonly showDesktopMenuToggle?: boolean;
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
defineSlots<{
|
|
38
|
+
default(): VNode[];
|
|
39
|
+
menu(): VNode[];
|
|
40
|
+
side(): VNode[];
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const route = useRoute();
|
|
44
|
+
const matchedMenuRoutes = useNamedRoutes(toRef(() => contextMenuName));
|
|
45
|
+
|
|
46
|
+
const isMenuCollapsed = useRemembered('application-menu-collapsed', true);
|
|
47
|
+
const layout = ref<FluxApplicationLayout>('default');
|
|
48
|
+
|
|
49
|
+
const totalLevels = computed(() => 1 + matchedMenuRoutes.value.length);
|
|
50
|
+
const viewIndex = ref(0);
|
|
51
|
+
|
|
52
|
+
function clampViewIndex(target: number): number {
|
|
53
|
+
if (target < 0) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const max = totalLevels.value - 1;
|
|
57
|
+
if (target > max) {
|
|
58
|
+
return max;
|
|
59
|
+
}
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function goToLevel(index: number): void {
|
|
64
|
+
viewIndex.value = clampViewIndex(index);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function goToMain(): void {
|
|
68
|
+
viewIndex.value = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function goToCurrent(): void {
|
|
72
|
+
viewIndex.value = totalLevels.value - 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function goToParent(): void {
|
|
76
|
+
viewIndex.value = clampViewIndex(viewIndex.value - 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function goToChild(): void {
|
|
80
|
+
viewIndex.value = clampViewIndex(viewIndex.value + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const contextStack = shallowRef<FluxApplicationContextInfo[]>([]);
|
|
84
|
+
const contexts = computed(() => contextStack.value);
|
|
85
|
+
const activeContext = computed(() => contextStack.value.at(-1));
|
|
86
|
+
|
|
87
|
+
function pushContext(info: FluxApplicationContextInfo): void {
|
|
88
|
+
contextStack.value = [...contextStack.value, info];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function removeContext(id: symbol): void {
|
|
92
|
+
contextStack.value = contextStack.value.filter(entry => entry.id !== id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
provide(FluxApplicationInjectionKey, {
|
|
96
|
+
activeContext,
|
|
97
|
+
contexts,
|
|
98
|
+
isMenuCollapsed,
|
|
99
|
+
layout,
|
|
100
|
+
showDesktopMenuToggle: toRef(() => showDesktopMenuToggle),
|
|
101
|
+
totalLevels,
|
|
102
|
+
viewIndex,
|
|
103
|
+
goToChild,
|
|
104
|
+
goToCurrent,
|
|
105
|
+
goToLevel,
|
|
106
|
+
goToMain,
|
|
107
|
+
goToParent,
|
|
108
|
+
pushContext,
|
|
109
|
+
removeContext
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
watch(() => route.fullPath, () => {
|
|
113
|
+
viewIndex.value = totalLevels.value - 1;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
watch(totalLevels, (next) => {
|
|
117
|
+
viewIndex.value = clampViewIndex(viewIndex.value);
|
|
118
|
+
|
|
119
|
+
// On initial route resolve, totalLevels jumps from 1 to its
|
|
120
|
+
// real value. Snap to the deepest level so the user lands on
|
|
121
|
+
// the right context without seeing a slide animation.
|
|
122
|
+
if (viewIndex.value === 0 && next > 1) {
|
|
123
|
+
viewIndex.value = next - 1;
|
|
124
|
+
}
|
|
125
|
+
}, {immediate: true});
|
|
126
|
+
|
|
127
|
+
watch(isMenuCollapsed, collapsed => {
|
|
128
|
+
if (typeof document === 'undefined') {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (collapsed) {
|
|
133
|
+
delete document.documentElement.dataset.applicationMenuOpen;
|
|
134
|
+
} else {
|
|
135
|
+
document.documentElement.dataset.applicationMenuOpen = '';
|
|
136
|
+
}
|
|
137
|
+
}, {immediate: true});
|
|
138
|
+
|
|
139
|
+
onUnmounted(() => {
|
|
140
|
+
if (typeof document !== 'undefined') {
|
|
141
|
+
delete document.documentElement.dataset.applicationMenuOpen;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main
|
|
3
|
+
:class="clsx(
|
|
4
|
+
layout === 'default' && $style.applicationContentDefault,
|
|
5
|
+
layout === 'dashboard' && $style.applicationContentDashboard,
|
|
6
|
+
layout === 'full' && $style.applicationContentFull,
|
|
7
|
+
layout === 'medium' && $style.applicationContentMedium,
|
|
8
|
+
layout === 'narrow' && $style.applicationContentNarrow
|
|
9
|
+
)"
|
|
10
|
+
aria-label="Application Content">
|
|
11
|
+
<slot/>
|
|
12
|
+
</main>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script
|
|
16
|
+
lang="ts"
|
|
17
|
+
setup>
|
|
18
|
+
import clsx from 'clsx';
|
|
19
|
+
import { useApplicationInjection } from '../composable';
|
|
20
|
+
import type { FluxApplicationLayout } from '../data';
|
|
21
|
+
import $style from '../css/component/ApplicationContent.module.scss';
|
|
22
|
+
import { watch } from 'vue';
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
layout = 'default'
|
|
26
|
+
} = defineProps<{
|
|
27
|
+
readonly layout?: FluxApplicationLayout;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
defineSlots<{
|
|
31
|
+
default(): any;
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
const {layout: layoutRef} = useApplicationInjection();
|
|
35
|
+
|
|
36
|
+
watch(() => layout, () => layoutRef.value = layout, {immediate: true});
|
|
37
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header :class="$style.applicationHero">
|
|
3
|
+
<slot name="start"/>
|
|
4
|
+
|
|
5
|
+
<div :class="$style.applicationHeroBody">
|
|
6
|
+
<h1>{{ title }}</h1>
|
|
7
|
+
|
|
8
|
+
<p
|
|
9
|
+
v-if="subtitle"
|
|
10
|
+
:class="$style.applicationHeroSubtitle">
|
|
11
|
+
{{ subtitle }}
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<slot name="end"/>
|
|
16
|
+
</header>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script
|
|
20
|
+
lang="ts"
|
|
21
|
+
setup>
|
|
22
|
+
import type { VNode } from 'vue';
|
|
23
|
+
import $style from '../css/component/ApplicationHero.module.scss';
|
|
24
|
+
|
|
25
|
+
defineProps<{
|
|
26
|
+
readonly title: string;
|
|
27
|
+
readonly subtitle?: string;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
defineSlots<{
|
|
31
|
+
start?(): VNode;
|
|
32
|
+
end?(): VNode;
|
|
33
|
+
}>();
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside
|
|
3
|
+
:class="$style.applicationMenu"
|
|
4
|
+
:data-collapsed="isMenuCollapsed ? '' : undefined"
|
|
5
|
+
:data-collapsible="showDesktopMenuToggle ? '' : undefined">
|
|
6
|
+
<FluxMenu
|
|
7
|
+
v-if="slots.header"
|
|
8
|
+
:class="$style.applicationMenuHeader">
|
|
9
|
+
<slot name="header"/>
|
|
10
|
+
</FluxMenu>
|
|
11
|
+
|
|
12
|
+
<div :class="$style.applicationMenuStage">
|
|
13
|
+
<div
|
|
14
|
+
:class="$style.applicationMenuTrack"
|
|
15
|
+
:style="{'--view-index': viewIndex}">
|
|
16
|
+
<FluxMenu :class="$style.applicationMenuPanel">
|
|
17
|
+
<slot/>
|
|
18
|
+
</FluxMenu>
|
|
19
|
+
|
|
20
|
+
<slot name="context"/>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<nav
|
|
25
|
+
v-if="showPageIndicator && totalLevels > 1"
|
|
26
|
+
:class="$style.applicationMenuPageIndicator"
|
|
27
|
+
aria-label="Menu levels">
|
|
28
|
+
<button
|
|
29
|
+
v-for="level in totalLevels"
|
|
30
|
+
:key="level - 1"
|
|
31
|
+
type="button"
|
|
32
|
+
:class="[
|
|
33
|
+
$style.applicationMenuPageIndicatorDot,
|
|
34
|
+
(level - 1) === viewIndex && $style.applicationMenuPageIndicatorDotActive
|
|
35
|
+
]"
|
|
36
|
+
:aria-current="(level - 1) === viewIndex ? 'true' : undefined"
|
|
37
|
+
:aria-label="(level - 1) === 0 ? 'Hoofdmenu' : `Niveau ${level - 1}`"
|
|
38
|
+
@click="goToLevel(level - 1)"/>
|
|
39
|
+
</nav>
|
|
40
|
+
|
|
41
|
+
<FluxMenu
|
|
42
|
+
v-if="slots.footer"
|
|
43
|
+
:class="$style.applicationMenuFooter"
|
|
44
|
+
:data-hidden="!isMainMenuVisible ? '' : undefined">
|
|
45
|
+
<slot name="footer"/>
|
|
46
|
+
</FluxMenu>
|
|
47
|
+
</aside>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script
|
|
51
|
+
lang="ts"
|
|
52
|
+
setup>
|
|
53
|
+
import { FluxMenu } from '@flux-ui/components';
|
|
54
|
+
import { type VNode } from 'vue';
|
|
55
|
+
import { useApplicationInjection, useApplicationMenu } from '../composable';
|
|
56
|
+
import $style from '../css/component/ApplicationMenu.module.scss';
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
showPageIndicator = true
|
|
60
|
+
} = defineProps<{
|
|
61
|
+
readonly showPageIndicator?: boolean;
|
|
62
|
+
}>();
|
|
63
|
+
|
|
64
|
+
const slots = defineSlots<{
|
|
65
|
+
default(): VNode;
|
|
66
|
+
context?(): VNode;
|
|
67
|
+
footer?(): VNode;
|
|
68
|
+
header?(): VNode;
|
|
69
|
+
}>();
|
|
70
|
+
|
|
71
|
+
const {isMenuCollapsed, showDesktopMenuToggle} = useApplicationInjection();
|
|
72
|
+
const {goToLevel, isMainMenuVisible, totalLevels, viewIndex} = useApplicationMenu();
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FluxFlyout is-auto-width>
|
|
3
|
+
<template #opener="{open}">
|
|
4
|
+
<FluxMenuItem
|
|
5
|
+
:class="slots.switcher ? $style.applicationMenuAccountSwitcher : $style.applicationMenuAccount"
|
|
6
|
+
:icon-leading="icon"
|
|
7
|
+
:icon-trailing="slots.switcher ? 'angle-down' : undefined"
|
|
8
|
+
:image-alt="imageAlt"
|
|
9
|
+
:image-src="imageSrc"
|
|
10
|
+
:label="label"
|
|
11
|
+
@click="slots.switcher && open()">
|
|
12
|
+
<template
|
|
13
|
+
v-if="slots.avatar"
|
|
14
|
+
#before>
|
|
15
|
+
<slot name="avatar"/>
|
|
16
|
+
</template>
|
|
17
|
+
</FluxMenuItem>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<template v-if="slots.switcher">
|
|
21
|
+
<FluxPane>
|
|
22
|
+
<slot name="switcher"/>
|
|
23
|
+
</FluxPane>
|
|
24
|
+
</template>
|
|
25
|
+
</FluxFlyout>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script
|
|
29
|
+
lang="ts"
|
|
30
|
+
setup>
|
|
31
|
+
import { FluxFlyout, FluxMenuItem, FluxPane } from '@flux-ui/components';
|
|
32
|
+
import type { FluxIconName } from '@flux-ui/types';
|
|
33
|
+
import type { VNode } from 'vue';
|
|
34
|
+
import $style from '../css/component/ApplicationMenu.module.scss';
|
|
35
|
+
|
|
36
|
+
defineProps<{
|
|
37
|
+
readonly icon?: FluxIconName;
|
|
38
|
+
readonly imageAlt?: string;
|
|
39
|
+
readonly imageSrc?: string;
|
|
40
|
+
readonly label: string;
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const slots = defineSlots<{
|
|
44
|
+
avatar?(): VNode;
|
|
45
|
+
switcher?(): VNode;
|
|
46
|
+
}>();
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="$style.applicationMenuContext">
|
|
3
|
+
<FluxSecondaryButton
|
|
4
|
+
icon-leading="angle-left"
|
|
5
|
+
size="small"
|
|
6
|
+
:tabindex="tabindex"
|
|
7
|
+
:href="canSlide ? undefined : href"
|
|
8
|
+
:rel="rel"
|
|
9
|
+
:target="target"
|
|
10
|
+
:to="canSlide ? undefined : to"
|
|
11
|
+
:type="canSlide ? 'button' : type"
|
|
12
|
+
@click="onBack"/>
|
|
13
|
+
|
|
14
|
+
<div :class="$style.applicationMenuContextContent">
|
|
15
|
+
<strong>{{ title }}</strong>
|
|
16
|
+
<span v-if="subtitle">{{ subtitle }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script
|
|
22
|
+
lang="ts"
|
|
23
|
+
setup>
|
|
24
|
+
import { FluxSecondaryButton } from '@flux-ui/components';
|
|
25
|
+
import type { FluxPressableType, FluxTo } from '@flux-ui/types';
|
|
26
|
+
import { computed, inject } from 'vue';
|
|
27
|
+
import { matchedRouteKey } from 'vue-router';
|
|
28
|
+
import { FluxApplicationInjectionKey } from '../data';
|
|
29
|
+
import useApplicationContextRegistration from '../composable/useApplicationContextRegistration';
|
|
30
|
+
import $style from '../css/component/ApplicationMenu.module.scss';
|
|
31
|
+
|
|
32
|
+
const props = defineProps<{
|
|
33
|
+
readonly subtitle?: string;
|
|
34
|
+
readonly title: string;
|
|
35
|
+
readonly tabindex?: string | number;
|
|
36
|
+
readonly href?: string;
|
|
37
|
+
readonly rel?: string;
|
|
38
|
+
readonly target?: string;
|
|
39
|
+
readonly to?: FluxTo;
|
|
40
|
+
readonly entryTo?: FluxTo;
|
|
41
|
+
readonly type?: FluxPressableType;
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
const injection = inject(FluxApplicationInjectionKey, null);
|
|
45
|
+
const matchedRoute = inject(matchedRouteKey, null);
|
|
46
|
+
|
|
47
|
+
const canSlide = computed(() => {
|
|
48
|
+
if (!injection) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return injection.viewIndex.value > 0;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const autoEntryTo = computed<FluxTo | undefined>(() => {
|
|
55
|
+
const record = matchedRoute?.value;
|
|
56
|
+
|
|
57
|
+
if (!record || typeof record.name !== 'string') {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {name: record.name};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function onBack(): void {
|
|
65
|
+
if (canSlide.value && injection) {
|
|
66
|
+
injection.goToParent();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
useApplicationContextRegistration(() => ({
|
|
71
|
+
title: props.title,
|
|
72
|
+
subtitle: props.subtitle,
|
|
73
|
+
to: props.to,
|
|
74
|
+
entryTo: props.entryTo ?? autoEntryTo.value,
|
|
75
|
+
href: props.href,
|
|
76
|
+
type: props.type
|
|
77
|
+
}));
|
|
78
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TransitionGroup
|
|
3
|
+
:enter-active-class="$style.applicationMenuPanelEnterActive"
|
|
4
|
+
:enter-from-class="$style.applicationMenuPanelEnterFrom"
|
|
5
|
+
:leave-active-class="$style.applicationMenuPanelLeaveActive"
|
|
6
|
+
:leave-to-class="$style.applicationMenuPanelLeaveTo">
|
|
7
|
+
<FluxMenu
|
|
8
|
+
v-for="match in matches"
|
|
9
|
+
:key="match.record.path"
|
|
10
|
+
:class="$style.applicationMenuPanel">
|
|
11
|
+
<ScopedRouterView
|
|
12
|
+
:depth="match.depth"
|
|
13
|
+
:view-name="name"/>
|
|
14
|
+
</FluxMenu>
|
|
15
|
+
</TransitionGroup>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script
|
|
19
|
+
lang="ts"
|
|
20
|
+
setup>
|
|
21
|
+
import { FluxMenu } from '@flux-ui/components';
|
|
22
|
+
import { defineComponent, h, provide, ref, toRef, type VNode } from 'vue';
|
|
23
|
+
import { RouterView, viewDepthKey } from 'vue-router';
|
|
24
|
+
import useNamedRoutes from '../routing/useNamedRoutes';
|
|
25
|
+
import $style from '../css/component/ApplicationMenu.module.scss';
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
name = 'menu'
|
|
29
|
+
} = defineProps<{
|
|
30
|
+
readonly name?: string;
|
|
31
|
+
}>();
|
|
32
|
+
|
|
33
|
+
const matches = useNamedRoutes(toRef(() => name));
|
|
34
|
+
|
|
35
|
+
const ScopedRouterView = defineComponent({
|
|
36
|
+
name: 'FluxApplicationMenuScopedRouterView',
|
|
37
|
+
props: {
|
|
38
|
+
depth: {
|
|
39
|
+
type: Number,
|
|
40
|
+
required: true
|
|
41
|
+
},
|
|
42
|
+
viewName: {
|
|
43
|
+
type: String,
|
|
44
|
+
required: true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
setup(props): () => VNode {
|
|
48
|
+
provide(viewDepthKey, ref(props.depth));
|
|
49
|
+
|
|
50
|
+
return () => h(RouterView, {name: props.viewName});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="$style.applicationMenuPromo">
|
|
3
|
+
<FluxIcon
|
|
4
|
+
v-if="icon"
|
|
5
|
+
:name="icon"/>
|
|
6
|
+
|
|
7
|
+
<div :class="$style.applicationMenuPromoContent">
|
|
8
|
+
<slot/>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script
|
|
14
|
+
lang="ts"
|
|
15
|
+
setup>
|
|
16
|
+
import { FluxIcon } from '@flux-ui/components';
|
|
17
|
+
import type { FluxIconName } from '@flux-ui/types';
|
|
18
|
+
import $style from '../css/component/ApplicationMenu.module.scss';
|
|
19
|
+
|
|
20
|
+
defineProps<{
|
|
21
|
+
readonly icon?: FluxIconName;
|
|
22
|
+
}>();
|
|
23
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FluxMenuItem
|
|
3
|
+
:class="$style.applicationMenuToggle"
|
|
4
|
+
@click="isMenuCollapsed = !isMenuCollapsed">
|
|
5
|
+
<template #before>
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
fill="none"
|
|
9
|
+
viewBox="0 0 18 18"
|
|
10
|
+
:class="$style.applicationMenuToggleIcon">
|
|
11
|
+
<path
|
|
12
|
+
fill-rule="evenodd"
|
|
13
|
+
d="M0 15V3a3 3 0 0 1 3-3h3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a3.04 3.04 0 0 1-3-3M2 3a1 1 0 0 1 1-1h2v14H3a1 1 0 0 1-1-1z"
|
|
14
|
+
clip-rule="evenodd"/>
|
|
15
|
+
|
|
16
|
+
<path d="M16 15V3a1 1 0 0 0-1-1h-5a1 1 0 0 1 0-2h5c1.63.04 3 1.33 3 3v12a3.07 3.07 0 0 1-3 3h-5a1 1 0 1 1 0-2h5a1 1 0 0 0 1-1"/>
|
|
17
|
+
</svg>
|
|
18
|
+
</template>
|
|
19
|
+
</FluxMenuItem>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script
|
|
23
|
+
lang="ts"
|
|
24
|
+
setup>
|
|
25
|
+
import { FluxMenuItem } from '@flux-ui/components';
|
|
26
|
+
import { useApplicationInjection } from '../composable';
|
|
27
|
+
import $style from '../css/component/ApplicationMenu.module.scss';
|
|
28
|
+
|
|
29
|
+
const {isMenuCollapsed} = useApplicationInjection();
|
|
30
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :class="$style.applicationSection">
|
|
3
|
+
<header
|
|
4
|
+
v-if="title || info || slots.end"
|
|
5
|
+
:class="$style.applicationSectionHeader">
|
|
6
|
+
<h2 v-if="title">
|
|
7
|
+
{{ title }}
|
|
8
|
+
</h2>
|
|
9
|
+
|
|
10
|
+
<slot name="end"/>
|
|
11
|
+
|
|
12
|
+
<span
|
|
13
|
+
v-if="info"
|
|
14
|
+
:class="$style.applicationSectionInfo">
|
|
15
|
+
{{ info }}
|
|
16
|
+
</span>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<div :class="$style.applicationSectionContent">
|
|
20
|
+
<slot/>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script
|
|
26
|
+
lang="ts"
|
|
27
|
+
setup>
|
|
28
|
+
import type { VNode } from 'vue';
|
|
29
|
+
import $style from '../css/component/ApplicationSection.module.scss';
|
|
30
|
+
|
|
31
|
+
defineProps<{
|
|
32
|
+
readonly title?: string;
|
|
33
|
+
readonly info?: string;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
const slots = defineSlots<{
|
|
37
|
+
default(): VNode;
|
|
38
|
+
end?(): VNode;
|
|
39
|
+
}>();
|
|
40
|
+
</script>
|