@tower_74/cms-app 0.1.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.
Files changed (189) hide show
  1. package/README.md +102 -0
  2. package/package.json +49 -0
  3. package/src/components/AppContent.vue +21 -0
  4. package/src/components/AppLogoIcon.vue +24 -0
  5. package/src/components/AppShell.vue +37 -0
  6. package/src/components/AppearanceTabs.vue +37 -0
  7. package/src/components/AuthBar.vue +58 -0
  8. package/src/components/BlockEditor.vue +95 -0
  9. package/src/components/DeleteUser.vue +87 -0
  10. package/src/components/FieldBuilder.vue +105 -0
  11. package/src/components/Heading.vue +20 -0
  12. package/src/components/HeadingSmall.vue +17 -0
  13. package/src/components/Icon.vue +30 -0
  14. package/src/components/InputError.vue +13 -0
  15. package/src/components/MenuItemsEditor.vue +59 -0
  16. package/src/components/NavUser.vue +30 -0
  17. package/src/components/Pagination.vue +28 -0
  18. package/src/components/PlaceholderPattern.vue +16 -0
  19. package/src/components/Seo.vue +28 -0
  20. package/src/components/TextLink.vue +24 -0
  21. package/src/components/UserInfo.vue +34 -0
  22. package/src/components/UserMenuContent.vue +37 -0
  23. package/src/components/commerce/OptionsEditor.vue +55 -0
  24. package/src/components/commerce/VariantsEditor.vue +71 -0
  25. package/src/components/ui/avatar/Avatar.vue +24 -0
  26. package/src/components/ui/avatar/AvatarFallback.vue +11 -0
  27. package/src/components/ui/avatar/AvatarImage.vue +9 -0
  28. package/src/components/ui/avatar/index.ts +24 -0
  29. package/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
  30. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +18 -0
  31. package/src/components/ui/breadcrumb/BreadcrumbItem.vue +14 -0
  32. package/src/components/ui/breadcrumb/BreadcrumbLink.vue +15 -0
  33. package/src/components/ui/breadcrumb/BreadcrumbList.vue +14 -0
  34. package/src/components/ui/breadcrumb/BreadcrumbPage.vue +14 -0
  35. package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +17 -0
  36. package/src/components/ui/breadcrumb/index.ts +7 -0
  37. package/src/components/ui/button/Button.vue +22 -0
  38. package/src/components/ui/button/index.ts +31 -0
  39. package/src/components/ui/card/Card.vue +14 -0
  40. package/src/components/ui/card/CardContent.vue +14 -0
  41. package/src/components/ui/card/CardDescription.vue +14 -0
  42. package/src/components/ui/card/CardFooter.vue +14 -0
  43. package/src/components/ui/card/CardHeader.vue +14 -0
  44. package/src/components/ui/card/CardTitle.vue +14 -0
  45. package/src/components/ui/card/index.ts +6 -0
  46. package/src/components/ui/checkbox/Checkbox.vue +36 -0
  47. package/src/components/ui/checkbox/index.ts +1 -0
  48. package/src/components/ui/collapsible/Collapsible.vue +15 -0
  49. package/src/components/ui/collapsible/CollapsibleContent.vue +14 -0
  50. package/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
  51. package/src/components/ui/collapsible/index.ts +3 -0
  52. package/src/components/ui/dialog/Dialog.vue +14 -0
  53. package/src/components/ui/dialog/DialogClose.vue +11 -0
  54. package/src/components/ui/dialog/DialogContent.vue +51 -0
  55. package/src/components/ui/dialog/DialogDescription.vue +21 -0
  56. package/src/components/ui/dialog/DialogFooter.vue +12 -0
  57. package/src/components/ui/dialog/DialogHeader.vue +14 -0
  58. package/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  59. package/src/components/ui/dialog/DialogTitle.vue +21 -0
  60. package/src/components/ui/dialog/DialogTrigger.vue +11 -0
  61. package/src/components/ui/dialog/index.ts +9 -0
  62. package/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
  63. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +42 -0
  64. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +40 -0
  65. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
  66. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +30 -0
  67. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +21 -0
  68. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +14 -0
  69. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +43 -0
  70. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +21 -0
  71. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
  72. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +14 -0
  73. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
  74. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  75. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
  76. package/src/components/ui/dropdown-menu/index.ts +16 -0
  77. package/src/components/ui/input/Input.vue +32 -0
  78. package/src/components/ui/input/index.ts +1 -0
  79. package/src/components/ui/label/Label.vue +22 -0
  80. package/src/components/ui/label/index.ts +1 -0
  81. package/src/components/ui/navigation-menu/NavigationMenu.vue +25 -0
  82. package/src/components/ui/navigation-menu/NavigationMenuContent.vue +31 -0
  83. package/src/components/ui/navigation-menu/NavigationMenuIndicator.vue +29 -0
  84. package/src/components/ui/navigation-menu/NavigationMenuItem.vue +11 -0
  85. package/src/components/ui/navigation-menu/NavigationMenuLink.vue +14 -0
  86. package/src/components/ui/navigation-menu/NavigationMenuList.vue +21 -0
  87. package/src/components/ui/navigation-menu/NavigationMenuTrigger.vue +24 -0
  88. package/src/components/ui/navigation-menu/NavigationMenuViewport.vue +29 -0
  89. package/src/components/ui/navigation-menu/index.ts +14 -0
  90. package/src/components/ui/separator/Separator.vue +31 -0
  91. package/src/components/ui/separator/index.ts +1 -0
  92. package/src/components/ui/sheet/Sheet.vue +14 -0
  93. package/src/components/ui/sheet/SheetClose.vue +11 -0
  94. package/src/components/ui/sheet/SheetContent.vue +53 -0
  95. package/src/components/ui/sheet/SheetDescription.vue +19 -0
  96. package/src/components/ui/sheet/SheetFooter.vue +12 -0
  97. package/src/components/ui/sheet/SheetHeader.vue +12 -0
  98. package/src/components/ui/sheet/SheetTitle.vue +19 -0
  99. package/src/components/ui/sheet/SheetTrigger.vue +11 -0
  100. package/src/components/ui/sheet/index.ts +29 -0
  101. package/src/components/ui/sidebar/Sidebar.vue +99 -0
  102. package/src/components/ui/sidebar/SidebarContent.vue +17 -0
  103. package/src/components/ui/sidebar/SidebarFooter.vue +14 -0
  104. package/src/components/ui/sidebar/SidebarGroup.vue +14 -0
  105. package/src/components/ui/sidebar/SidebarGroupAction.vue +31 -0
  106. package/src/components/ui/sidebar/SidebarGroupContent.vue +14 -0
  107. package/src/components/ui/sidebar/SidebarGroupLabel.vue +29 -0
  108. package/src/components/ui/sidebar/SidebarHeader.vue +14 -0
  109. package/src/components/ui/sidebar/SidebarInput.vue +15 -0
  110. package/src/components/ui/sidebar/SidebarInset.vue +22 -0
  111. package/src/components/ui/sidebar/SidebarMenu.vue +14 -0
  112. package/src/components/ui/sidebar/SidebarMenuAction.vue +41 -0
  113. package/src/components/ui/sidebar/SidebarMenuBadge.vue +27 -0
  114. package/src/components/ui/sidebar/SidebarMenuButton.vue +52 -0
  115. package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +33 -0
  116. package/src/components/ui/sidebar/SidebarMenuItem.vue +14 -0
  117. package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +22 -0
  118. package/src/components/ui/sidebar/SidebarMenuSub.vue +23 -0
  119. package/src/components/ui/sidebar/SidebarMenuSubButton.vue +42 -0
  120. package/src/components/ui/sidebar/SidebarMenuSubItem.vue +7 -0
  121. package/src/components/ui/sidebar/SidebarProvider.vue +89 -0
  122. package/src/components/ui/sidebar/SidebarRail.vue +34 -0
  123. package/src/components/ui/sidebar/SidebarSeparator.vue +15 -0
  124. package/src/components/ui/sidebar/SidebarTrigger.vue +20 -0
  125. package/src/components/ui/sidebar/index.ts +51 -0
  126. package/src/components/ui/sidebar/utils.ts +19 -0
  127. package/src/components/ui/skeleton/Skeleton.vue +14 -0
  128. package/src/components/ui/skeleton/index.ts +1 -0
  129. package/src/components/ui/tooltip/Tooltip.vue +14 -0
  130. package/src/components/ui/tooltip/TooltipContent.vue +39 -0
  131. package/src/components/ui/tooltip/TooltipProvider.vue +11 -0
  132. package/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
  133. package/src/components/ui/tooltip/index.ts +4 -0
  134. package/src/composables/useAppearance.ts +53 -0
  135. package/src/composables/useInitials.ts +14 -0
  136. package/src/index.ts +22 -0
  137. package/src/layouts/AdminLayout.vue +170 -0
  138. package/src/layouts/AuthLayout.vue +14 -0
  139. package/src/layouts/PublicLayout.vue +53 -0
  140. package/src/layouts/auth/AuthCardLayout.vue +36 -0
  141. package/src/layouts/auth/AuthSimpleLayout.vue +31 -0
  142. package/src/layouts/auth/AuthSplitLayout.vue +40 -0
  143. package/src/layouts/settings/Layout.vue +56 -0
  144. package/src/lib/utils.ts +6 -0
  145. package/src/pages/Admin/Appearance/Theme.vue +58 -0
  146. package/src/pages/Admin/Appearance/Widgets.vue +48 -0
  147. package/src/pages/Admin/Commerce/Orders/Index.vue +80 -0
  148. package/src/pages/Admin/Commerce/Orders/Show.vue +200 -0
  149. package/src/pages/Admin/Commerce/Products/Edit.vue +167 -0
  150. package/src/pages/Admin/Commerce/Products/Index.vue +65 -0
  151. package/src/pages/Admin/Content/Edit.vue +170 -0
  152. package/src/pages/Admin/Content/Index.vue +88 -0
  153. package/src/pages/Admin/Content/Preview.vue +25 -0
  154. package/src/pages/Admin/Dashboard.vue +26 -0
  155. package/src/pages/Admin/Forms/Edit.vue +98 -0
  156. package/src/pages/Admin/Forms/Index.vue +68 -0
  157. package/src/pages/Admin/Forms/Submissions/Index.vue +68 -0
  158. package/src/pages/Admin/Forms/Submissions/Show.vue +47 -0
  159. package/src/pages/Admin/Media/Index.vue +75 -0
  160. package/src/pages/Admin/Menus/Create.vue +37 -0
  161. package/src/pages/Admin/Menus/Edit.vue +54 -0
  162. package/src/pages/Admin/Menus/Index.vue +52 -0
  163. package/src/pages/Admin/Settings/Index.vue +184 -0
  164. package/src/pages/Admin/Taxonomy/Edit.vue +83 -0
  165. package/src/pages/Admin/Taxonomy/Index.vue +68 -0
  166. package/src/pages/Admin/Users/Edit.vue +82 -0
  167. package/src/pages/Admin/Users/Index.vue +74 -0
  168. package/src/pages/Public/Cart/Index.vue +108 -0
  169. package/src/pages/Public/Checkout/Confirmation.vue +110 -0
  170. package/src/pages/Public/Checkout/Index.vue +174 -0
  171. package/src/pages/Public/Index.vue +54 -0
  172. package/src/pages/Public/Shop/Index.vue +39 -0
  173. package/src/pages/Public/Shop/Show.vue +46 -0
  174. package/src/pages/Public/Show.vue +41 -0
  175. package/src/pages/Setup/Complete.vue +53 -0
  176. package/src/pages/Setup/Index.vue +85 -0
  177. package/src/pages/Welcome.vue +787 -0
  178. package/src/pages/auth/ConfirmPassword.vue +53 -0
  179. package/src/pages/auth/ForgotPassword.vue +54 -0
  180. package/src/pages/auth/Login.vue +91 -0
  181. package/src/pages/auth/Register.vue +83 -0
  182. package/src/pages/auth/ResetPassword.vue +81 -0
  183. package/src/pages/auth/VerifyEmail.vue +36 -0
  184. package/src/pages/settings/Appearance.vue +23 -0
  185. package/src/pages/settings/Password.vue +120 -0
  186. package/src/pages/settings/Profile.vue +105 -0
  187. package/src/pages.ts +9 -0
  188. package/src/types/index.ts +42 -0
  189. package/src/types/ziggy.ts +12 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @tower_74/cms-app
2
+
3
+ Front-end **app shell** for the Base CMS — the Inertia/Vue pages, layouts, components,
4
+ composables and helpers that make up the admin area and public site. It is the front-end
5
+ counterpart to the `tower74/cms-core` Composer package and a sibling to
6
+ [`@tower_74/cms-ui`](https://github.com/JMancuso79/cms-ui) (the shared primitive/widget
7
+ library this shell composes).
8
+
9
+ Client sites consume this as a **versioned dependency** and update the front-end core with a
10
+ `npm update` instead of merging (see ADR-0023). Any page can be **overridden per site**.
11
+
12
+ > **Ships source, not a build.** Unlike `cms-ui` (a compiled component library), this package
13
+ > ships its raw `src/` (`.vue` + `import.meta.glob`). The consuming app's Vite compiles it, so
14
+ > Tailwind can scan its classes and the Inertia page glob can be transformed. That requires the
15
+ > wiring below.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @tower_74/cms-app
21
+ ```
22
+
23
+ Peer dependencies (Vue, Inertia, Pinia, radix-vue, lucide, `@tower_74/cms-ui`, ziggy, …) must
24
+ be present in the host app — they are in the CMS skeleton already.
25
+
26
+ ## Wiring a host app
27
+
28
+ ### 1. Vite (`vite.config.ts`)
29
+
30
+ ```ts
31
+ resolve: {
32
+ alias: {
33
+ // The package's own `@/...` imports resolve against its src.
34
+ '@': path.resolve(__dirname, './node_modules/@tower_74/cms-app/src'),
35
+ },
36
+ },
37
+ // Ship-source package: let Vite (not esbuild) process it so its import.meta.glob transforms.
38
+ optimizeDeps: { exclude: ['@tower_74/cms-app'] },
39
+ ssr: { noExternal: ['@tower_74/cms-app'] },
40
+ ```
41
+
42
+ ### 2. Entry points (`resources/js/app.ts` + `ssr.ts`)
43
+
44
+ ```ts
45
+ import { initializeTheme, resolveCmsPage } from '@tower_74/cms-app';
46
+
47
+ createInertiaApp({
48
+ // Core pages come from the package; this app's own ./pages override by name.
49
+ resolve: (name) => resolveCmsPage(name, import.meta.glob('./pages/**/*.vue')),
50
+ // ...
51
+ });
52
+ ```
53
+
54
+ ### 3. Tailwind (`tailwind.config.js`)
55
+
56
+ ```js
57
+ content: [
58
+ // ...your app's own templates
59
+ './node_modules/@tower_74/cms-app/src/**/*.{vue,js,ts,jsx,tsx}',
60
+ './node_modules/@tower_74/cms-ui/dist/**/*.{js,cjs}',
61
+ ],
62
+ ```
63
+
64
+ ### 4. Inertia testing (`config/inertia.php`)
65
+
66
+ The PHP test suite asserts pages exist on disk via a path independent of Vite. Point it at the
67
+ package (and your override dir):
68
+
69
+ ```php
70
+ 'testing' => [
71
+ 'ensure_pages_exist' => true,
72
+ 'page_paths' => [
73
+ base_path('node_modules/@tower_74/cms-app/src/pages'),
74
+ resource_path('js/pages'),
75
+ ],
76
+ 'page_extensions' => ['vue'],
77
+ ],
78
+ ```
79
+
80
+ ## Overriding a page
81
+
82
+ Drop a same-named file into the host app's `resources/js/pages/**`. Both globs produce
83
+ `./pages/Name.vue` keys, and `resolveCmsPage` layers the local glob over the package's, so the
84
+ local file wins. No core files are edited.
85
+
86
+ ## Layout
87
+
88
+ ```
89
+ src/
90
+ index.ts # public API: resolveCmsPage(), initializeTheme, cmsPages
91
+ pages.ts # import.meta.glob of all shipped pages
92
+ pages/ # Admin/**, Public/**, Auth/**, Setup/**, settings/**
93
+ layouts/ # AdminLayout, PublicLayout, AuthLayout, …
94
+ components/ # app-shell components (+ vendored shadcn-vue ui/)
95
+ composables/ # useAppearance, useInitials
96
+ lib/ # utils
97
+ types/ # shared TS types
98
+ ```
99
+
100
+ ## License
101
+
102
+ UNLICENSED — proprietary. Distributed for use by Base CMS client sites.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@tower_74/cms-app",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "private": false,
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "exports": {
12
+ ".": "./src/index.ts",
13
+ "./pages": "./src/pages.ts"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/JMancuso79/cms-app.git"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "lint": "eslint . --fix",
24
+ "format": "prettier --write src/",
25
+ "format:check": "prettier --check src/"
26
+ },
27
+ "peerDependencies": {
28
+ "@inertiajs/vue3": "^2.0.0-beta.3",
29
+ "@tower_74/cms-ui": "^0.12.0",
30
+ "@vueuse/core": "^12.0.0",
31
+ "laravel-vite-plugin": "^1.0",
32
+ "lucide-vue-next": "^0.468.0",
33
+ "pinia": "^3.0.4",
34
+ "radix-vue": "^1.9.11",
35
+ "vue": "^3.5.13",
36
+ "ziggy-js": "^2.4.2"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.19.0",
40
+ "@vue/eslint-config-typescript": "^14.3.0",
41
+ "eslint": "^9.17.0",
42
+ "eslint-config-prettier": "^10.0.1",
43
+ "eslint-plugin-vue": "^9.32.0",
44
+ "prettier": "^3.4.2",
45
+ "typescript": "^5.2.2",
46
+ "typescript-eslint": "^8.23.0",
47
+ "vue": "^3.5.13"
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { SidebarInset } from '@/components/ui/sidebar';
3
+ import { computed } from 'vue';
4
+
5
+ interface Props {
6
+ variant?: 'header' | 'sidebar';
7
+ class?: string;
8
+ }
9
+
10
+ const props = defineProps<Props>();
11
+ const className = computed(() => props.class);
12
+ </script>
13
+
14
+ <template>
15
+ <SidebarInset v-if="props.variant === 'sidebar'" :class="className">
16
+ <slot />
17
+ </SidebarInset>
18
+ <main v-else class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl" :class="className">
19
+ <slot />
20
+ </main>
21
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+
4
+ defineOptions({
5
+ inheritAttrs: false,
6
+ });
7
+
8
+ interface Props {
9
+ className?: HTMLAttributes['class'];
10
+ }
11
+
12
+ defineProps<Props>();
13
+ </script>
14
+
15
+ <template>
16
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" :class="className" v-bind="$attrs">
17
+ <path
18
+ fill="currentColor"
19
+ fill-rule="evenodd"
20
+ clip-rule="evenodd"
21
+ d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
22
+ />
23
+ </svg>
24
+ </template>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import { SidebarProvider } from '@/components/ui/sidebar';
3
+ import { onMounted, ref } from 'vue';
4
+
5
+ interface Props {
6
+ variant?: 'header' | 'sidebar';
7
+ }
8
+
9
+ defineProps<Props>();
10
+
11
+ const isOpen = ref(true);
12
+
13
+ onMounted(() => {
14
+ isOpen.value = localStorage.getItem('sidebar') !== 'false';
15
+ });
16
+
17
+ const handleSidebarChange = (open: boolean) => {
18
+ isOpen.value = open;
19
+ localStorage.setItem('sidebar', String(open));
20
+ };
21
+ </script>
22
+
23
+ <template>
24
+ <div v-if="variant === 'header'" class="flex min-h-screen w-full flex-col">
25
+ <slot />
26
+ </div>
27
+ <div v-else class="flex min-h-svh flex-col">
28
+ <!-- Full-width row above the sidebar (e.g. the authenticated admin bar). Pinned so it
29
+ stays aligned with the fixed sidebar, which is offset down by the bar's height. -->
30
+ <div class="sticky top-0 z-30">
31
+ <slot name="topbar" />
32
+ </div>
33
+ <SidebarProvider :default-open="isOpen" :open="isOpen" class="min-h-0 flex-1" @update:open="handleSidebarChange">
34
+ <slot />
35
+ </SidebarProvider>
36
+ </div>
37
+ </template>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import { useAppearance } from '@/composables/useAppearance';
3
+ import { Monitor, Moon, Sun } from 'lucide-vue-next';
4
+
5
+ interface Props {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: containerClass = '' } = defineProps<Props>();
10
+
11
+ const { appearance, updateAppearance } = useAppearance();
12
+
13
+ const tabs = [
14
+ { value: 'light', Icon: Sun, label: 'Light' },
15
+ { value: 'dark', Icon: Moon, label: 'Dark' },
16
+ { value: 'system', Icon: Monitor, label: 'System' },
17
+ ] as const;
18
+ </script>
19
+
20
+ <template>
21
+ <div :class="['inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800', containerClass]">
22
+ <button
23
+ v-for="{ value, Icon, label } in tabs"
24
+ :key="value"
25
+ @click="updateAppearance(value)"
26
+ :class="[
27
+ 'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
28
+ appearance === value
29
+ ? 'bg-white shadow-sm dark:bg-neutral-700 dark:text-neutral-100'
30
+ : 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
31
+ ]"
32
+ >
33
+ <component :is="Icon" class="-ml-1 h-4 w-4" />
34
+ <span class="ml-1.5 text-sm">{{ label }}</span>
35
+ </button>
36
+ </div>
37
+ </template>
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { Link, usePage } from '@inertiajs/vue3';
3
+ import { ExternalLink, LayoutGrid, LogOut, Settings, SquarePen } from 'lucide-vue-next';
4
+ import { computed } from 'vue';
5
+
6
+ // Thin dark bar (WordPress-style) shown only to signed-in users. The primary link points the
7
+ // other way depending on context: "Dashboard" on the public site, "View site" in the admin.
8
+ // `edit` is a server-resolved link to the current content's admin editor — present only when
9
+ // the signed-in user is allowed to edit it (PostPolicy::update), so we render it whenever set.
10
+ const props = withDefaults(
11
+ defineProps<{
12
+ primaryLabel: string;
13
+ primaryHref: string;
14
+ primaryKind?: 'dashboard' | 'site';
15
+ edit?: { url: string; label: string } | null;
16
+ }>(),
17
+ {
18
+ primaryKind: 'dashboard',
19
+ edit: null,
20
+ },
21
+ );
22
+
23
+ const page = usePage();
24
+ const user = computed(() => (page.props.auth as { user?: { name: string } | null } | undefined)?.user ?? null);
25
+ const primaryIcon = computed(() => (props.primaryKind === 'site' ? ExternalLink : LayoutGrid));
26
+ </script>
27
+
28
+ <template>
29
+ <div v-if="user" class="bg-neutral-900 text-neutral-300">
30
+ <div class="flex h-8 items-center justify-between gap-4 px-4 text-xs sm:px-6">
31
+ <div class="flex items-center gap-3">
32
+ <Link :href="primaryHref" class="flex items-center gap-1.5 font-medium transition-colors hover:text-white">
33
+ <component :is="primaryIcon" class="size-3.5" />
34
+ {{ primaryLabel }}
35
+ </Link>
36
+ <template v-if="edit">
37
+ <span class="text-neutral-600">·</span>
38
+ <Link :href="edit.url" class="flex items-center gap-1.5 transition-colors hover:text-white">
39
+ <SquarePen class="size-3.5" />
40
+ {{ edit.label }}
41
+ </Link>
42
+ </template>
43
+ </div>
44
+ <div class="flex items-center gap-3">
45
+ <Link :href="route('profile.edit')" class="flex items-center gap-1.5 transition-colors hover:text-white">
46
+ <Settings class="size-3.5" />
47
+ <span class="hidden sm:inline">Settings</span>
48
+ </Link>
49
+ <span class="text-neutral-600">·</span>
50
+ <span class="hidden text-neutral-400 sm:inline">{{ user.name }}</span>
51
+ <Link :href="route('logout')" method="post" as="button" class="flex items-center gap-1.5 transition-colors hover:text-white">
52
+ <LogOut class="size-3.5" />
53
+ <span class="hidden sm:inline">Log out</span>
54
+ </Link>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </template>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { Button, FormBuilder, type MediaItem } from '@tower_74/cms-ui';
3
+ import { ref } from 'vue';
4
+
5
+ interface FieldSpec {
6
+ name: string;
7
+ label?: string;
8
+ type?: string;
9
+ required?: boolean;
10
+ options?: Array<{ label: string; value: string | number }>;
11
+ }
12
+ interface BlockDef {
13
+ type: string;
14
+ label: string;
15
+ fields: FieldSpec[];
16
+ defaults: Record<string, unknown>;
17
+ }
18
+ interface Block {
19
+ type: string;
20
+ data: Record<string, unknown>;
21
+ }
22
+
23
+ const props = withDefaults(defineProps<{ modelValue: Block[]; registry: BlockDef[]; mediaItems?: MediaItem[] }>(), {
24
+ mediaItems: () => [],
25
+ });
26
+ const emit = defineEmits<{ 'update:modelValue': [value: Block[]]; upload: [file: File] }>();
27
+
28
+ const newType = ref(props.registry[0]?.type ?? '');
29
+
30
+ const defFor = (type: string) => props.registry.find((d) => d.type === type);
31
+ const labelFor = (type: string) => defFor(type)?.label ?? type;
32
+
33
+ const commit = (blocks: Block[]) => emit('update:modelValue', blocks);
34
+
35
+ const addBlock = () => {
36
+ const def = defFor(newType.value);
37
+ if (!def) return;
38
+ commit([...props.modelValue, { type: def.type, data: { ...def.defaults } }]);
39
+ };
40
+
41
+ const removeBlock = (index: number) => {
42
+ const blocks = [...props.modelValue];
43
+ blocks.splice(index, 1);
44
+ commit(blocks);
45
+ };
46
+
47
+ const move = (index: number, direction: -1 | 1) => {
48
+ const target = index + direction;
49
+ if (target < 0 || target >= props.modelValue.length) return;
50
+ const blocks = [...props.modelValue];
51
+ [blocks[index], blocks[target]] = [blocks[target], blocks[index]];
52
+ commit(blocks);
53
+ };
54
+
55
+ const setData = (index: number, data: Record<string, unknown>) => {
56
+ const blocks = [...props.modelValue];
57
+ blocks[index] = { ...blocks[index], data };
58
+ commit(blocks);
59
+ };
60
+ </script>
61
+
62
+ <template>
63
+ <div class="space-y-4">
64
+ <div class="flex items-center gap-2">
65
+ <select v-model="newType" class="h-10 rounded border border-border bg-background px-3 text-sm text-text">
66
+ <option v-for="def in registry" :key="def.type" :value="def.type">{{ def.label }}</option>
67
+ </select>
68
+ <Button type="button" variant="secondary" @click="addBlock">Add block</Button>
69
+ </div>
70
+
71
+ <p v-if="!modelValue.length" class="rounded border border-dashed border-border px-4 py-8 text-center text-sm text-muted">
72
+ No blocks yet. Pick a type above and add your first block.
73
+ </p>
74
+
75
+ <div v-for="(block, index) in modelValue" :key="index" class="rounded-lg border border-border bg-background">
76
+ <div class="flex items-center justify-between border-b border-border px-4 py-2">
77
+ <span class="text-sm font-medium">{{ labelFor(block.type) }}</span>
78
+ <div class="flex items-center gap-1">
79
+ <Button type="button" variant="ghost" size="sm" :disabled="index === 0" @click="move(index, -1)">↑</Button>
80
+ <Button type="button" variant="ghost" size="sm" :disabled="index === modelValue.length - 1" @click="move(index, 1)">↓</Button>
81
+ <Button type="button" variant="danger" size="sm" @click="removeBlock(index)">Remove</Button>
82
+ </div>
83
+ </div>
84
+ <div class="p-4">
85
+ <FormBuilder
86
+ :model-value="block.data"
87
+ :fields="(defFor(block.type)?.fields ?? []) as never"
88
+ :media-items="mediaItems"
89
+ @update:model-value="setData(index, $event)"
90
+ @upload="emit('upload', $event)"
91
+ />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </template>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import { useForm } from '@inertiajs/vue3';
3
+ import { ref } from 'vue';
4
+
5
+ // Components
6
+ import HeadingSmall from '@/components/HeadingSmall.vue';
7
+ import InputError from '@/components/InputError.vue';
8
+ import { Button } from '@/components/ui/button';
9
+ import {
10
+ Dialog,
11
+ DialogClose,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogFooter,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogTrigger,
18
+ } from '@/components/ui/dialog';
19
+ import { Input } from '@/components/ui/input';
20
+ import { Label } from '@/components/ui/label';
21
+
22
+ const passwordInput = ref<HTMLInputElement | null>(null);
23
+
24
+ const form = useForm({
25
+ password: '',
26
+ });
27
+
28
+ const deleteUser = (e: Event) => {
29
+ e.preventDefault();
30
+
31
+ form.delete(route('profile.destroy'), {
32
+ preserveScroll: true,
33
+ onSuccess: () => closeModal(),
34
+ onError: () => passwordInput.value?.focus(),
35
+ onFinish: () => form.reset(),
36
+ });
37
+ };
38
+
39
+ const closeModal = () => {
40
+ form.clearErrors();
41
+ form.reset();
42
+ };
43
+ </script>
44
+
45
+ <template>
46
+ <div class="space-y-6">
47
+ <HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
48
+ <div class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
49
+ <div class="relative space-y-0.5 text-red-600 dark:text-red-100">
50
+ <p class="font-medium">Warning</p>
51
+ <p class="text-sm">Please proceed with caution, this cannot be undone.</p>
52
+ </div>
53
+ <Dialog>
54
+ <DialogTrigger as-child>
55
+ <Button variant="destructive">Delete account</Button>
56
+ </DialogTrigger>
57
+ <DialogContent>
58
+ <form class="space-y-6" @submit="deleteUser">
59
+ <DialogHeader class="space-y-3">
60
+ <DialogTitle>Are you sure you want to delete your account?</DialogTitle>
61
+ <DialogDescription>
62
+ Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your
63
+ password to confirm you would like to permanently delete your account.
64
+ </DialogDescription>
65
+ </DialogHeader>
66
+
67
+ <div class="grid gap-2">
68
+ <Label for="password" class="sr-only">Password</Label>
69
+ <Input id="password" type="password" name="password" ref="passwordInput" v-model="form.password" placeholder="Password" />
70
+ <InputError :message="form.errors.password" />
71
+ </div>
72
+
73
+ <DialogFooter>
74
+ <DialogClose as-child>
75
+ <Button variant="secondary" @click="closeModal"> Cancel </Button>
76
+ </DialogClose>
77
+
78
+ <Button variant="destructive" :disabled="form.processing">
79
+ <button type="submit">Delete account</button>
80
+ </Button>
81
+ </DialogFooter>
82
+ </form>
83
+ </DialogContent>
84
+ </Dialog>
85
+ </div>
86
+ </div>
87
+ </template>
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts">
2
+ import { Button, Checkbox, Input, Select } from '@tower_74/cms-ui';
3
+
4
+ interface Option {
5
+ label: string;
6
+ value: string;
7
+ }
8
+ interface Field {
9
+ name?: string;
10
+ label: string;
11
+ type: string;
12
+ required?: boolean;
13
+ placeholder?: string;
14
+ options?: Option[];
15
+ }
16
+
17
+ const props = defineProps<{
18
+ modelValue: Field[];
19
+ fieldTypes: Array<{ value: string; label: string }>;
20
+ }>();
21
+ const emit = defineEmits<{ 'update:modelValue': [value: Field[]] }>();
22
+
23
+ const OPTION_TYPES = ['select', 'radio'];
24
+
25
+ const commit = (fields: Field[]) => emit('update:modelValue', fields);
26
+
27
+ const addField = () => commit([...props.modelValue, { label: 'New field', type: 'text', required: false, placeholder: '' }]);
28
+ const removeField = (i: number) => {
29
+ const fields = [...props.modelValue];
30
+ fields.splice(i, 1);
31
+ commit(fields);
32
+ };
33
+ const move = (i: number, dir: -1 | 1) => {
34
+ const target = i + dir;
35
+ if (target < 0 || target >= props.modelValue.length) return;
36
+ const fields = [...props.modelValue];
37
+ [fields[i], fields[target]] = [fields[target], fields[i]];
38
+ commit(fields);
39
+ };
40
+ const setField = (i: number, patch: Partial<Field>) => {
41
+ const fields = [...props.modelValue];
42
+ fields[i] = { ...fields[i], ...patch };
43
+ commit(fields);
44
+ };
45
+
46
+ const addOption = (i: number) => setField(i, { options: [...(props.modelValue[i].options ?? []), { label: '', value: '' }] });
47
+ const setOption = (i: number, oi: number, patch: Partial<Option>) => {
48
+ const options = [...(props.modelValue[i].options ?? [])];
49
+ options[oi] = { ...options[oi], ...patch };
50
+ setField(i, { options });
51
+ };
52
+ const removeOption = (i: number, oi: number) => {
53
+ const options = [...(props.modelValue[i].options ?? [])];
54
+ options.splice(oi, 1);
55
+ setField(i, { options });
56
+ };
57
+ </script>
58
+
59
+ <template>
60
+ <div class="space-y-3">
61
+ <div v-for="(field, i) in modelValue" :key="i" class="rounded-lg border border-border bg-background p-4">
62
+ <div class="mb-3 flex items-center justify-between">
63
+ <span class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Field {{ i + 1 }}</span>
64
+ <div class="flex items-center gap-1">
65
+ <Button type="button" variant="ghost" size="sm" :disabled="i === 0" @click="move(i, -1)">↑</Button>
66
+ <Button type="button" variant="ghost" size="sm" :disabled="i === modelValue.length - 1" @click="move(i, 1)">↓</Button>
67
+ <Button type="button" variant="danger" size="sm" @click="removeField(i)">Remove</Button>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="grid gap-3 sm:grid-cols-2">
72
+ <label class="block">
73
+ <span class="mb-1 block text-sm font-medium text-foreground">Label</span>
74
+ <Input :model-value="field.label" @update:model-value="setField(i, { label: $event })" />
75
+ </label>
76
+ <label class="block">
77
+ <span class="mb-1 block text-sm font-medium text-foreground">Type</span>
78
+ <Select :model-value="field.type" :options="fieldTypes" @update:model-value="setField(i, { type: $event })" />
79
+ </label>
80
+ </div>
81
+
82
+ <label v-if="!['checkbox', 'select', 'radio'].includes(field.type)" class="mt-3 block">
83
+ <span class="mb-1 block text-sm font-medium text-foreground">Placeholder</span>
84
+ <Input :model-value="field.placeholder ?? ''" @update:model-value="setField(i, { placeholder: $event })" />
85
+ </label>
86
+
87
+ <!-- Options for select / radio -->
88
+ <div v-if="OPTION_TYPES.includes(field.type)" class="mt-3">
89
+ <span class="mb-1 block text-sm font-medium text-foreground">Options</span>
90
+ <div v-for="(opt, oi) in field.options ?? []" :key="oi" class="mb-2 flex items-center gap-2">
91
+ <Input :model-value="opt.label" placeholder="Label" @update:model-value="setOption(i, oi, { label: $event })" />
92
+ <Input :model-value="opt.value" placeholder="Value" @update:model-value="setOption(i, oi, { value: $event })" />
93
+ <Button type="button" variant="ghost" size="sm" @click="removeOption(i, oi)">✕</Button>
94
+ </div>
95
+ <Button type="button" variant="secondary" size="sm" @click="addOption(i)">Add option</Button>
96
+ </div>
97
+
98
+ <div class="mt-3">
99
+ <Checkbox :model-value="!!field.required" label="Required" @update:model-value="setField(i, { required: $event })" />
100
+ </div>
101
+ </div>
102
+
103
+ <Button type="button" variant="secondary" @click="addField">Add field</Button>
104
+ </div>
105
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import { Separator } from '@/components/ui/separator';
3
+
4
+ interface Props {
5
+ title: string;
6
+ description?: string;
7
+ }
8
+
9
+ defineProps<Props>();
10
+ </script>
11
+
12
+ <template>
13
+ <div class="mb-8 space-y-0.5">
14
+ <h2 class="text-xl font-semibold tracking-tight">{{ title }}</h2>
15
+ <p v-if="description" class="text-sm text-muted-foreground">
16
+ {{ description }}
17
+ </p>
18
+ </div>
19
+ <Separator class="my-6" />
20
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ title: string;
4
+ description?: string;
5
+ }
6
+
7
+ defineProps<Props>();
8
+ </script>
9
+
10
+ <template>
11
+ <header>
12
+ <h3 class="mb-0.5 text-base font-medium">{{ title }}</h3>
13
+ <p v-if="description" class="text-sm text-muted-foreground">
14
+ {{ description }}
15
+ </p>
16
+ </header>
17
+ </template>