@tower_74/cms-app 0.1.0 → 0.3.0
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/package.json +5 -3
- package/src/index.ts +8 -5
- package/src/layouts/AdminLayout.vue +17 -1
- package/src/layouts/PublicLayout.vue +2 -1
- package/src/pages/Admin/Content/Preview.vue +2 -1
- package/src/pages/Public/Shop/Show.vue +2 -1
- package/src/pages/Public/Show.vue +3 -2
- package/src/plugins.ts +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tower_74/cms-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Front-end app shell for the Base CMS — Inertia/Vue pages, layouts, components and composables. Ships source; consumed by client sites as a versioned dependency (ADR-0023) and overridable per site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@inertiajs/vue3": "^2.0.0-beta.3",
|
|
29
|
-
"@tower_74/cms-ui": "^0.
|
|
29
|
+
"@tower_74/cms-ui": "^0.13.0",
|
|
30
30
|
"@vueuse/core": "^12.0.0",
|
|
31
31
|
"laravel-vite-plugin": "^1.0",
|
|
32
32
|
"lucide-vue-next": "^0.468.0",
|
|
@@ -42,8 +42,10 @@
|
|
|
42
42
|
"eslint-config-prettier": "^10.0.1",
|
|
43
43
|
"eslint-plugin-vue": "^9.32.0",
|
|
44
44
|
"prettier": "^3.4.2",
|
|
45
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
45
46
|
"typescript": "^5.2.2",
|
|
46
47
|
"typescript-eslint": "^8.23.0",
|
|
47
|
-
"vue": "^3.5.13"
|
|
48
|
+
"vue": "^3.5.13",
|
|
49
|
+
"vue-tsc": "^3.3.5"
|
|
48
50
|
}
|
|
49
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
|
2
2
|
import type { DefineComponent } from 'vue';
|
|
3
3
|
import { cmsPages } from './pages';
|
|
4
|
+
import { pluginPages } from './plugins';
|
|
4
5
|
|
|
5
6
|
export { initializeTheme } from './composables/useAppearance';
|
|
7
|
+
export { pluginBlocks, pluginIcons, registerCmsPlugins } from './plugins';
|
|
8
|
+
export type { CmsPlugin } from './plugins';
|
|
6
9
|
export { cmsPages };
|
|
7
10
|
|
|
8
11
|
type PageGlob = Record<string, () => Promise<unknown>>;
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
|
-
* Resolve an Inertia page by name
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `./pages/Name.vue` keys
|
|
15
|
-
* by dropping a same-named file into its own `resources/js/pages`.
|
|
14
|
+
* Resolve an Inertia page by name. Sources merge in precedence order (later wins): the
|
|
15
|
+
* CMS app shell's own pages → installed plugins' pages (ADR-0026) → the host app's
|
|
16
|
+
* `localPages` (a client overrides any page by dropping a same-named file in its own
|
|
17
|
+
* `resources/js/pages`). All globs produce `./pages/Name.vue` keys.
|
|
16
18
|
*/
|
|
17
19
|
export function resolveCmsPage(name: string, localPages: PageGlob = {}) {
|
|
18
20
|
return resolvePageComponent(`./pages/${name}.vue`, {
|
|
19
21
|
...cmsPages,
|
|
22
|
+
...pluginPages(),
|
|
20
23
|
...localPages,
|
|
21
24
|
} as Record<string, () => Promise<DefineComponent>>);
|
|
22
25
|
}
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
SidebarMenuItem,
|
|
17
17
|
SidebarTrigger,
|
|
18
18
|
} from '@/components/ui/sidebar';
|
|
19
|
+
import { pluginIcons } from '@/plugins';
|
|
19
20
|
import { Link, usePage } from '@inertiajs/vue3';
|
|
20
21
|
import {
|
|
22
|
+
Box,
|
|
21
23
|
FileStack,
|
|
22
24
|
FileText,
|
|
23
25
|
Folder,
|
|
@@ -73,7 +75,21 @@ const site = computed(
|
|
|
73
75
|
// Name shows whenever there's no logo, or the logo is configured to sit beside the name.
|
|
74
76
|
const showName = computed(() => !site.value.logo || site.value.showName);
|
|
75
77
|
const can = computed(() => (page.props.auth as { can?: Record<string, boolean> } | undefined)?.can ?? {});
|
|
76
|
-
|
|
78
|
+
// Admin-nav items contributed by installed plugins (ADR-0026), shared from the server as
|
|
79
|
+
// `adminNav` with a lucide icon *name* the plugin's front end resolves (fallback: Box).
|
|
80
|
+
const pluginNav = computed<NavItem[]>(() => {
|
|
81
|
+
const icons = pluginIcons();
|
|
82
|
+
const items = (page.props.adminNav as Array<{ label: string; href: string; icon: string | null; permission: string | null }> | undefined) ?? [];
|
|
83
|
+
|
|
84
|
+
return items.map((item) => ({
|
|
85
|
+
label: item.label,
|
|
86
|
+
href: item.href,
|
|
87
|
+
icon: (item.icon ? icons[item.icon] : undefined) ?? Box,
|
|
88
|
+
permission: item.permission ?? undefined,
|
|
89
|
+
}));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const visibleNav = computed(() => [...nav, ...pluginNav.value].filter((item) => !item.permission || can.value[item.permission]));
|
|
77
93
|
|
|
78
94
|
const isActive = (href: string) => (href === '/dashboard' ? url.value === '/dashboard' : url.value.startsWith(href));
|
|
79
95
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import AuthBar from '@/components/AuthBar.vue';
|
|
3
|
+
import { pluginBlocks } from '@/plugins';
|
|
3
4
|
import { type Block, BlockRenderer } from '@tower_74/cms-ui';
|
|
4
5
|
import { computed } from 'vue';
|
|
5
6
|
|
|
@@ -46,7 +47,7 @@ const showName = computed(() => !props.site.logo || props.site.showName);
|
|
|
46
47
|
</main>
|
|
47
48
|
|
|
48
49
|
<footer class="border-t border-border bg-surface">
|
|
49
|
-
<BlockRenderer v-if="footerWidgets.length" :blocks="footerWidgets" />
|
|
50
|
+
<BlockRenderer v-if="footerWidgets.length" :blocks="footerWidgets" :components="pluginBlocks()" />
|
|
50
51
|
<div class="mx-auto max-w-6xl px-4 py-6 text-sm text-muted sm:px-6">© {{ year }} {{ site.name }}</div>
|
|
51
52
|
</footer>
|
|
52
53
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pluginBlocks } from '@/plugins';
|
|
2
3
|
import { Link } from '@inertiajs/vue3';
|
|
3
4
|
import { type Block, Button, LandingPageTemplate } from '@tower_74/cms-ui';
|
|
4
5
|
|
|
@@ -19,7 +20,7 @@ defineProps<{
|
|
|
19
20
|
</Link>
|
|
20
21
|
</div>
|
|
21
22
|
|
|
22
|
-
<LandingPageTemplate v-if="blocks.length" :blocks="blocks" />
|
|
23
|
+
<LandingPageTemplate v-if="blocks.length" :blocks="blocks" :components="pluginBlocks()" />
|
|
23
24
|
<p v-else class="px-4 py-20 text-center text-muted">This page has no blocks yet. Add some in the editor.</p>
|
|
24
25
|
</div>
|
|
25
26
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import Seo from '@/components/Seo.vue';
|
|
3
3
|
import PublicLayout from '@/layouts/PublicLayout.vue';
|
|
4
|
+
import { pluginBlocks } from '@/plugins';
|
|
4
5
|
import { router } from '@inertiajs/vue3';
|
|
5
6
|
import { type Block, BlockRenderer, ProductOverview } from '@tower_74/cms-ui';
|
|
6
7
|
|
|
@@ -41,6 +42,6 @@ const addToCart = (payload: { variantId: number | string; quantity: number }) =>
|
|
|
41
42
|
@add-to-cart="addToCart"
|
|
42
43
|
/>
|
|
43
44
|
|
|
44
|
-
<BlockRenderer v-if="product.blocks.length" :blocks="product.blocks" />
|
|
45
|
+
<BlockRenderer v-if="product.blocks.length" :blocks="product.blocks" :components="pluginBlocks()" />
|
|
45
46
|
</PublicLayout>
|
|
46
47
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import Seo from '@/components/Seo.vue';
|
|
3
3
|
import PublicLayout from '@/layouts/PublicLayout.vue';
|
|
4
|
+
import { pluginBlocks } from '@/plugins';
|
|
4
5
|
import { type Block, DetailPageTemplate, LandingPageTemplate } from '@tower_74/cms-ui';
|
|
5
6
|
|
|
6
7
|
defineProps<{
|
|
@@ -29,13 +30,13 @@ defineProps<{
|
|
|
29
30
|
<div v-if="page.showTitle" class="mx-auto max-w-6xl px-4 pb-2 pt-10 sm:px-6">
|
|
30
31
|
<h1 class="text-3xl font-bold tracking-tight text-text">{{ page.title }}</h1>
|
|
31
32
|
</div>
|
|
32
|
-
<LandingPageTemplate :blocks="page.blocks" />
|
|
33
|
+
<LandingPageTemplate :blocks="page.blocks" :components="pluginBlocks()" />
|
|
33
34
|
</template>
|
|
34
35
|
<template v-else>
|
|
35
36
|
<div v-if="page.featuredImage" class="mx-auto max-w-3xl px-4 pt-8 sm:px-6">
|
|
36
37
|
<img :src="page.featuredImage.src" :alt="page.featuredImage.alt ?? ''" class="w-full rounded-lg object-cover" />
|
|
37
38
|
</div>
|
|
38
|
-
<DetailPageTemplate :header="page.header" :blocks="page.blocks" />
|
|
39
|
+
<DetailPageTemplate :header="page.header" :blocks="page.blocks" :components="pluginBlocks()" />
|
|
39
40
|
</template>
|
|
40
41
|
</PublicLayout>
|
|
41
42
|
</template>
|
package/src/plugins.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
|
+
|
|
3
|
+
type PageGlob = Record<string, () => Promise<unknown>>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A plugin's front-end contribution (ADR-0026). The plugin's npm package exports one of
|
|
7
|
+
* these; the host registers them all in `resources/js/cms.plugins.ts` via
|
|
8
|
+
* {@link registerCmsPlugins}. Pages are merged into the Inertia resolver, blocks into the
|
|
9
|
+
* block renderer, and icons resolve the names a plugin's admin-nav items reference.
|
|
10
|
+
*/
|
|
11
|
+
export interface CmsPlugin {
|
|
12
|
+
/** Machine key, matching the PHP plugin (e.g. "events"). */
|
|
13
|
+
key: string;
|
|
14
|
+
/** The plugin's pages: `import.meta.glob('./pages/**\/*.vue')` from its own package. */
|
|
15
|
+
pages?: PageGlob;
|
|
16
|
+
/** Block type → Vue component, for the block renderer. */
|
|
17
|
+
blocks?: Record<string, Component>;
|
|
18
|
+
/** lucide icon name → component, for the admin-nav items the plugin registers (PHP side). */
|
|
19
|
+
icons?: Record<string, Component>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const registered: CmsPlugin[] = [];
|
|
23
|
+
|
|
24
|
+
/** Register the front-end of the plugins this site installs. Call once, at app boot. */
|
|
25
|
+
export function registerCmsPlugins(plugins: CmsPlugin[]): void {
|
|
26
|
+
registered.push(...plugins);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Merged page globs across all registered plugins (later plugins win on a key clash). */
|
|
30
|
+
export function pluginPages(): PageGlob {
|
|
31
|
+
return Object.assign({}, ...registered.map((p) => p.pages ?? {}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Merged block type → component map across all registered plugins. */
|
|
35
|
+
export function pluginBlocks(): Record<string, Component> {
|
|
36
|
+
return Object.assign({}, ...registered.map((p) => p.blocks ?? {}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Merged icon-name → component map across all registered plugins (for admin nav). */
|
|
40
|
+
export function pluginIcons(): Record<string, Component> {
|
|
41
|
+
return Object.assign({}, ...registered.map((p) => p.icons ?? {}));
|
|
42
|
+
}
|