@tower_74/cms-app 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tower_74/cms-app",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -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';
6
7
  export { cmsPages };
8
+ export { pluginBlocks, pluginIcons, registerCmsPlugins } from './plugins';
9
+ export type { CmsPlugin } from './plugins';
7
10
 
8
11
  type PageGlob = Record<string, () => Promise<unknown>>;
9
12
 
10
13
  /**
11
- * Resolve an Inertia page by name, preferring a client site's own page of the same
12
- * name over the one shipped by the CMS app shell (ADR-0023). `localPages` is the host
13
- * app's `import.meta.glob('./pages/**\/*.vue')` and may be empty. Both globs produce
14
- * `./pages/Name.vue` keys, so spreading local on top lets a client override any page
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,22 @@ 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
- const visibleNav = computed(() => nav.filter((item) => !item.permission || can.value[item.permission]));
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 =
83
+ (page.props.adminNav as Array<{ label: string; href: string; icon: string | null; permission: string | null }> | undefined) ?? [];
84
+
85
+ return items.map((item) => ({
86
+ label: item.label,
87
+ href: item.href,
88
+ icon: (item.icon ? icons[item.icon] : undefined) ?? Box,
89
+ permission: item.permission ?? undefined,
90
+ }));
91
+ });
92
+
93
+ const visibleNav = computed(() => [...nav, ...pluginNav.value].filter((item) => !item.permission || can.value[item.permission]));
77
94
 
78
95
  const isActive = (href: string) => (href === '/dashboard' ? url.value === '/dashboard' : url.value.startsWith(href));
79
96
 
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
+ }