kmcom-nuxt-layers 1.6.0 → 1.6.2
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/layers/ui/app/app.config.ts +8 -0
- package/layers/ui/app/components/Base/Modal.vue +111 -0
- package/layers/ui/app/components/Mast/Nav.vue +15 -3
- package/layers/ui/app/components/Mast/NavModal.vue +70 -0
- package/layers/ui/app/composables/mastNav.ts +25 -0
- package/layers/ui/app/composables/toast.ts +22 -0
- package/layers/ui/app/layouts/default.vue +1 -0
- package/layers/ui/app/utils/createModal.ts +34 -0
- package/package.json +16 -18
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export default defineAppConfig({
|
|
2
|
+
mastNav: {
|
|
3
|
+
links: [] as Array<{ id: string; label: string; to: string | { name: string; params?: Record<string, unknown>; query?: Record<string, unknown> } }>,
|
|
4
|
+
scrollBehaviour: 'router' as 'smooth-scroll' | 'router',
|
|
5
|
+
},
|
|
2
6
|
uiLayer: {
|
|
3
7
|
gradients: {
|
|
4
8
|
brand: { shape: 'linear', direction: 'to-br', from: { color: 'primary', shade: 500 }, to: { color: 'secondary', shade: 600 } },
|
|
@@ -35,6 +39,10 @@ export default defineAppConfig({
|
|
|
35
39
|
|
|
36
40
|
declare module '@nuxt/schema' {
|
|
37
41
|
interface AppConfigInput {
|
|
42
|
+
mastNav?: {
|
|
43
|
+
links?: Array<{ id: string; label: string; to: string | { name: string; params?: Record<string, unknown>; query?: Record<string, unknown> } }>
|
|
44
|
+
scrollBehaviour?: 'smooth-scroll' | 'router'
|
|
45
|
+
}
|
|
38
46
|
uiLayer?: {
|
|
39
47
|
name?: string
|
|
40
48
|
gradients?: Record<string, import('./types/gradient').GradientConfig>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BaseModal — overlay-compatible modal shell.
|
|
4
|
+
*
|
|
5
|
+
* Use this as a starting point for new modals:
|
|
6
|
+
* 1. Copy or extend this component
|
|
7
|
+
* 2. Add your props and content in the slots
|
|
8
|
+
* 3. Register with createModal() in a composable
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* // components/ConfirmModal.vue
|
|
12
|
+
* <BaseModal title="Are you sure?" @confirm="..." />
|
|
13
|
+
*
|
|
14
|
+
* // composables/confirmModal.ts
|
|
15
|
+
* export const useConfirmModal = createModal(ConfirmModal)
|
|
16
|
+
*/
|
|
17
|
+
const { open = false, title, description, size = 'md' } = defineProps<{
|
|
18
|
+
open?: boolean
|
|
19
|
+
title?: string
|
|
20
|
+
description?: string
|
|
21
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
'update:open': [value: boolean]
|
|
26
|
+
'close': []
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const sizeClass: Record<string, string> = {
|
|
30
|
+
sm: 'max-w-sm',
|
|
31
|
+
md: 'max-w-md',
|
|
32
|
+
lg: 'max-w-lg',
|
|
33
|
+
xl: 'max-w-xl',
|
|
34
|
+
full: 'max-w-full',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dismiss() {
|
|
38
|
+
emit('update:open', false)
|
|
39
|
+
emit('close')
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<Transition name="base-modal">
|
|
45
|
+
<div
|
|
46
|
+
v-if="open"
|
|
47
|
+
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
48
|
+
@click.self="dismiss"
|
|
49
|
+
>
|
|
50
|
+
<!-- Backdrop -->
|
|
51
|
+
<div class="bg-(--ui-bg-muted) absolute inset-0 opacity-60" @click="dismiss" />
|
|
52
|
+
|
|
53
|
+
<!-- Panel -->
|
|
54
|
+
<div
|
|
55
|
+
class="bg-(--ui-bg) ring-(--ui-border) relative w-full rounded-2xl shadow-xl ring-1"
|
|
56
|
+
:class="sizeClass[size]"
|
|
57
|
+
>
|
|
58
|
+
<!-- Header -->
|
|
59
|
+
<div v-if="title || $slots.header" class="border-(--ui-border) flex items-start justify-between border-b px-6 py-4">
|
|
60
|
+
<slot name="header">
|
|
61
|
+
<div>
|
|
62
|
+
<p class="text-(--ui-text) text-base font-semibold">
|
|
63
|
+
{{ title }}
|
|
64
|
+
</p>
|
|
65
|
+
<p v-if="description" class="text-(--ui-text-muted) mt-0.5 text-sm">
|
|
66
|
+
{{ description }}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
</slot>
|
|
70
|
+
<button
|
|
71
|
+
class="text-(--ui-text-muted) hover:text-(--ui-text) ml-4 cursor-pointer border-0 bg-transparent p-1 transition-colors"
|
|
72
|
+
aria-label="Close"
|
|
73
|
+
@click="dismiss"
|
|
74
|
+
>
|
|
75
|
+
<UIcon name="lucide:x" class="size-5" />
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Body -->
|
|
80
|
+
<div class="px-6 py-5">
|
|
81
|
+
<slot />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Footer -->
|
|
85
|
+
<div v-if="$slots.footer" class="border-(--ui-border) flex justify-end gap-3 border-t px-6 py-4">
|
|
86
|
+
<slot name="footer" :dismiss="dismiss" />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</Transition>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<style scoped>
|
|
94
|
+
.base-modal-enter-active,
|
|
95
|
+
.base-modal-leave-active {
|
|
96
|
+
transition: opacity 200ms ease;
|
|
97
|
+
}
|
|
98
|
+
.base-modal-enter-from,
|
|
99
|
+
.base-modal-leave-to {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
}
|
|
102
|
+
.base-modal-enter-active .bg-\(--ui-bg\),
|
|
103
|
+
.base-modal-leave-active .bg-\(--ui-bg\) {
|
|
104
|
+
transition: opacity 200ms ease, transform 200ms ease;
|
|
105
|
+
}
|
|
106
|
+
.base-modal-enter-from .bg-\(--ui-bg\),
|
|
107
|
+
.base-modal-leave-to .bg-\(--ui-bg\) {
|
|
108
|
+
opacity: 0;
|
|
109
|
+
transform: scale(0.96) translateY(4px);
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { open } = useMastNav()
|
|
3
|
+
const z = useGridConfig().useZIndex('dropdown')
|
|
4
|
+
</script>
|
|
5
|
+
|
|
1
6
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
<button
|
|
8
|
+
aria-label="Open navigation menu"
|
|
9
|
+
class="fixed right-0 top-0 flex cursor-pointer items-center justify-center border-0 bg-transparent p-4"
|
|
10
|
+
:style="{ zIndex: z }"
|
|
11
|
+
@click="open"
|
|
12
|
+
>
|
|
13
|
+
<slot name="icon">
|
|
14
|
+
<UIcon name="lucide:menu" class="size-7" />
|
|
15
|
+
</slot>
|
|
16
|
+
</button>
|
|
5
17
|
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { open } = defineProps<{ open?: boolean }>()
|
|
3
|
+
const emit = defineEmits<{
|
|
4
|
+
'update:open': [value: boolean]
|
|
5
|
+
'close': []
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const { links, scrollBehaviour } = useAppConfig().mastNav
|
|
9
|
+
const activeSection = useState<string>('activeSection', () => '')
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const { close: closeNav } = useMastNav()
|
|
12
|
+
|
|
13
|
+
function dismiss() {
|
|
14
|
+
emit('update:open', false)
|
|
15
|
+
emit('close')
|
|
16
|
+
closeNav()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function handleNav(link: { id: string; to: string | { name: string; params?: Record<string, unknown>; query?: Record<string, unknown> } }) {
|
|
20
|
+
if (scrollBehaviour === 'smooth-scroll' && route.name === 'index') {
|
|
21
|
+
try { useSmoothScroll().scrollTo(`#${link.id}`) }
|
|
22
|
+
catch {}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
navigateTo(link.to)
|
|
26
|
+
}
|
|
27
|
+
dismiss()
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<Transition name="nav-modal">
|
|
33
|
+
<div
|
|
34
|
+
v-if="open"
|
|
35
|
+
class="bg-(--ui-bg) text-(--ui-text) fixed inset-0 flex flex-col items-center justify-center"
|
|
36
|
+
:style="{ zIndex: useGridConfig().useZIndex('modal') }"
|
|
37
|
+
>
|
|
38
|
+
<button
|
|
39
|
+
class="absolute right-[5vw] top-[3vh] cursor-pointer border-0 bg-transparent p-2"
|
|
40
|
+
aria-label="Close navigation"
|
|
41
|
+
@click="dismiss"
|
|
42
|
+
>
|
|
43
|
+
<UIcon name="lucide:x" class="size-7" />
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<LinksGroup tag="nav" class="flex flex-col items-center gap-2">
|
|
47
|
+
<button
|
|
48
|
+
v-for="link in links"
|
|
49
|
+
:key="link.id"
|
|
50
|
+
class="cursor-pointer border-0 bg-transparent px-4 py-3 text-3xl uppercase tracking-widest transition-all"
|
|
51
|
+
:class="activeSection === link.id ? 'font-normal' : 'font-light'"
|
|
52
|
+
@click="handleNav(link)"
|
|
53
|
+
>
|
|
54
|
+
{{ link.label }}
|
|
55
|
+
</button>
|
|
56
|
+
</LinksGroup>
|
|
57
|
+
</div>
|
|
58
|
+
</Transition>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.nav-modal-enter-active,
|
|
63
|
+
.nav-modal-leave-active {
|
|
64
|
+
transition: opacity 300ms ease;
|
|
65
|
+
}
|
|
66
|
+
.nav-modal-enter-from,
|
|
67
|
+
.nav-modal-leave-to {
|
|
68
|
+
opacity: 0;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createSharedComposable } from '@vueuse/core'
|
|
2
|
+
import MastNavModal from '../components/Mast/NavModal.vue'
|
|
3
|
+
|
|
4
|
+
function _useMastNav() {
|
|
5
|
+
if (import.meta.server) return { open: () => {}, close: () => {} }
|
|
6
|
+
|
|
7
|
+
const overlay = useOverlay()
|
|
8
|
+
const modal = overlay.create(MastNavModal)
|
|
9
|
+
|
|
10
|
+
function open() {
|
|
11
|
+
try { useSmoothScroll().lockScrolling() }
|
|
12
|
+
catch {}
|
|
13
|
+
modal.open()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function close() {
|
|
17
|
+
modal.close()
|
|
18
|
+
try { useSmoothScroll().unlockScrolling() }
|
|
19
|
+
catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { open, close }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useMastNav = createSharedComposable(_useMastNav)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type ToastOptions = {
|
|
2
|
+
description?: string
|
|
3
|
+
duration?: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function useAppToast() {
|
|
7
|
+
const toast = useToast()
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
success: (title: string, opts?: ToastOptions) =>
|
|
11
|
+
toast.add({ title, icon: 'lucide:check-circle', color: 'success', ...opts }),
|
|
12
|
+
|
|
13
|
+
error: (title: string, opts?: ToastOptions) =>
|
|
14
|
+
toast.add({ title, icon: 'lucide:x-circle', color: 'error', ...opts }),
|
|
15
|
+
|
|
16
|
+
info: (title: string, opts?: ToastOptions) =>
|
|
17
|
+
toast.add({ title, icon: 'lucide:info', color: 'info', ...opts }),
|
|
18
|
+
|
|
19
|
+
warning: (title: string, opts?: ToastOptions) =>
|
|
20
|
+
toast.add({ title, icon: 'lucide:triangle-alert', color: 'warning', ...opts }),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createSharedComposable } from '@vueuse/core'
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory that turns any overlay-compatible component into a shared modal composable.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* // composables/myModal.ts
|
|
9
|
+
* import MyModal from '../components/MyModal.vue'
|
|
10
|
+
* export const useMyModal = createModal(MyModal)
|
|
11
|
+
*
|
|
12
|
+
* // anywhere in the app
|
|
13
|
+
* const { open, close, patch } = useMyModal()
|
|
14
|
+
* open({ title: 'Hello' })
|
|
15
|
+
*
|
|
16
|
+
* The returned composable is wrapped with createSharedComposable so overlay.create()
|
|
17
|
+
* is only called once regardless of how many components call it.
|
|
18
|
+
*
|
|
19
|
+
* The modal component must accept `open: boolean` and emit `update:open` + `close`.
|
|
20
|
+
* Extend BaseModal.vue as a starting point.
|
|
21
|
+
*/
|
|
22
|
+
export function createModal<P extends Record<string, unknown>>(component: Component) {
|
|
23
|
+
return createSharedComposable(() => {
|
|
24
|
+
if (import.meta.server) return { open: () => {}, close: () => {}, patch: () => {} }
|
|
25
|
+
|
|
26
|
+
const overlay = useOverlay()
|
|
27
|
+
const modal = overlay.create<P>(component)
|
|
28
|
+
return {
|
|
29
|
+
open: (props?: Partial<P>) => modal.open(props),
|
|
30
|
+
close: () => modal.close(),
|
|
31
|
+
patch: (props: Partial<P>) => modal.patch(props),
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kmcom-nuxt-layers",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.2",
|
|
5
5
|
"description": "Composable Nuxt 4 layers for building scalable Vue applications",
|
|
6
6
|
"files": [
|
|
7
7
|
"layers/*/nuxt.config.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"nuxt": "^4.4.2",
|
|
39
39
|
"nuxt-studio": "^1.4.0",
|
|
40
40
|
"pinia": "^3.0.4",
|
|
41
|
-
"tailwindcss": "^4.2.
|
|
41
|
+
"tailwindcss": "^4.2.2",
|
|
42
42
|
"three": "^0.183.2",
|
|
43
43
|
"zod": "^4.3.6"
|
|
44
44
|
},
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@eslint/json": "^1.0.1",
|
|
94
94
|
"@eslint/markdown": "^7.5.1",
|
|
95
95
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
|
96
|
-
"@iconify-json/lucide": "^1.2.
|
|
96
|
+
"@iconify-json/lucide": "^1.2.102",
|
|
97
97
|
"@nuxt/eslint": "^1.15.2",
|
|
98
98
|
"@nuxt/fonts": "^0.14.0",
|
|
99
99
|
"@nuxt/image": "^2.0.0",
|
|
@@ -101,35 +101,33 @@
|
|
|
101
101
|
"@nuxtjs/device": "^4.0.0",
|
|
102
102
|
"@perplex-digital/stylelint-config": "^17.3.0",
|
|
103
103
|
"@pinia/nuxt": "^0.11.3",
|
|
104
|
-
"@types/node": "^25.
|
|
105
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
106
|
-
"@typescript-eslint/parser": "^8.
|
|
104
|
+
"@types/node": "^25.6.0",
|
|
105
|
+
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
106
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
107
107
|
"@vue/eslint-config-typescript": "^14.7.0",
|
|
108
108
|
"@vueuse/core": "^14.2.1",
|
|
109
109
|
"@vueuse/nuxt": "^14.2.1",
|
|
110
110
|
"@webgpu/glslang": "^0.0.15",
|
|
111
|
-
"browserslist": "^4.28.
|
|
111
|
+
"browserslist": "^4.28.2",
|
|
112
112
|
"changesets": "^1.0.2",
|
|
113
113
|
"cypress": "^15.10.0",
|
|
114
114
|
"depcheck": "^1.4.7",
|
|
115
|
-
"eslint": "^10.0
|
|
116
|
-
"eslint-plugin-compat": "^
|
|
115
|
+
"eslint": "^10.2.0",
|
|
116
|
+
"eslint-plugin-compat": "^7.0.1",
|
|
117
117
|
"eslint-plugin-glsl": "0.0.0-wip",
|
|
118
|
-
"eslint-plugin-oxlint": "^1.50.0",
|
|
119
118
|
"eslint-plugin-pnpm": "^1.6.0",
|
|
120
119
|
"eslint-plugin-prettier": "^5.5.5",
|
|
121
|
-
"eslint-plugin-unicorn": "^
|
|
120
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
122
121
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
123
122
|
"eslint-plugin-vue": "^10.8.0",
|
|
124
123
|
"npm-check-updates": "^19.6.3",
|
|
125
124
|
"nuxt": "latest",
|
|
126
|
-
"oxlint": "^1.43.0",
|
|
127
125
|
"pinia": "^3.0.4",
|
|
128
126
|
"playwright": "^1.58.2",
|
|
129
127
|
"postcss-html": "^1.8.1",
|
|
130
|
-
"prettier": "^3.8.
|
|
128
|
+
"prettier": "^3.8.2",
|
|
131
129
|
"prettier-plugin-css-order": "^2.2.0",
|
|
132
|
-
"prettier-plugin-glsl": "^0.2.
|
|
130
|
+
"prettier-plugin-glsl": "^0.2.5",
|
|
133
131
|
"prettier-plugin-organize-attributes": "^1.0.0",
|
|
134
132
|
"prettier-plugin-tailwind-styled-components": "^2.0.2",
|
|
135
133
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
@@ -140,13 +138,13 @@
|
|
|
140
138
|
"stylelint-config-tailwindcss": "^1.0.1",
|
|
141
139
|
"stylelint-no-unsupported-browser-features": "^8.1.1",
|
|
142
140
|
"stylelint-prettier": "^5.0.3",
|
|
143
|
-
"tailwindcss": "^4.2.
|
|
141
|
+
"tailwindcss": "^4.2.2",
|
|
144
142
|
"turbo": "^2.8.13",
|
|
145
|
-
"typescript": "^
|
|
143
|
+
"typescript": "^6.0.2",
|
|
146
144
|
"vite-plugin-checker": "^0.12.0",
|
|
147
145
|
"vitest": "^4.0.18",
|
|
148
146
|
"vue": "latest",
|
|
149
|
-
"vue-tsc": "^3.2.
|
|
147
|
+
"vue-tsc": "^3.2.6",
|
|
150
148
|
"zod": "^4.3.6",
|
|
151
149
|
"zod-to-json-schema": "^3.25.1"
|
|
152
150
|
},
|
|
@@ -157,7 +155,7 @@
|
|
|
157
155
|
"last 2 Edge major versions"
|
|
158
156
|
],
|
|
159
157
|
"dependencies": {
|
|
160
|
-
"skills": "^1.
|
|
158
|
+
"skills": "^1.5.0"
|
|
161
159
|
},
|
|
162
160
|
"engines": {
|
|
163
161
|
"node": ">=18 <21"
|