@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
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import { useEventListener, useMediaQuery, useVModel } from '@vueuse/core';
4
+ import { TooltipProvider } from 'radix-vue';
5
+ import { computed, ref, type HTMLAttributes, type Ref } from 'vue';
6
+ import {
7
+ SIDEBAR_COOKIE_MAX_AGE,
8
+ SIDEBAR_COOKIE_NAME,
9
+ SIDEBAR_KEYBOARD_SHORTCUT,
10
+ SIDEBAR_WIDTH,
11
+ SIDEBAR_WIDTH_ICON,
12
+ provideSidebarContext,
13
+ } from './utils';
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ defaultOpen?: boolean;
18
+ open?: boolean;
19
+ class?: HTMLAttributes['class'];
20
+ }>(),
21
+ {
22
+ defaultOpen: true,
23
+ open: undefined,
24
+ },
25
+ );
26
+
27
+ const emits = defineEmits<{
28
+ 'update:open': [open: boolean];
29
+ }>();
30
+
31
+ const isMobile = useMediaQuery('(max-width: 768px)');
32
+ const openMobile = ref(false);
33
+
34
+ const open = useVModel(props, 'open', emits, {
35
+ defaultValue: props.defaultOpen ?? false,
36
+ passive: (props.open === undefined) as false,
37
+ }) as Ref<boolean>;
38
+
39
+ function setOpen(value: boolean) {
40
+ open.value = value; // emits('update:open', value)
41
+
42
+ // This sets the cookie to keep the sidebar state.
43
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
44
+ }
45
+
46
+ function setOpenMobile(value: boolean) {
47
+ openMobile.value = value;
48
+ }
49
+
50
+ // Helper to toggle the sidebar.
51
+ function toggleSidebar() {
52
+ return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value);
53
+ }
54
+
55
+ useEventListener('keydown', (event: KeyboardEvent) => {
56
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
57
+ event.preventDefault();
58
+ toggleSidebar();
59
+ }
60
+ });
61
+
62
+ // We add a state so that we can do data-state="expanded" or "collapsed".
63
+ // This makes it easier to style the sidebar with Tailwind classes.
64
+ const state = computed(() => (open.value ? 'expanded' : 'collapsed'));
65
+
66
+ provideSidebarContext({
67
+ state,
68
+ open,
69
+ setOpen,
70
+ isMobile,
71
+ openMobile,
72
+ setOpenMobile,
73
+ toggleSidebar,
74
+ });
75
+ </script>
76
+
77
+ <template>
78
+ <TooltipProvider :delay-duration="0">
79
+ <div
80
+ :style="{
81
+ '--sidebar-width': SIDEBAR_WIDTH,
82
+ '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
83
+ }"
84
+ :class="cn('group/sidebar-wrapper flex min-h-svh w-full text-sidebar-foreground has-[[data-variant=inset]]:bg-sidebar', props.class)"
85
+ >
86
+ <slot />
87
+ </div>
88
+ </TooltipProvider>
89
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { useSidebar } from './utils';
5
+
6
+ const props = defineProps<{
7
+ class?: HTMLAttributes['class'];
8
+ }>();
9
+
10
+ const { toggleSidebar } = useSidebar();
11
+ </script>
12
+
13
+ <template>
14
+ <button
15
+ data-sidebar="rail"
16
+ aria-label="Toggle Sidebar"
17
+ :tabindex="-1"
18
+ title="Toggle Sidebar"
19
+ :class="
20
+ cn(
21
+ 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
22
+ '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
23
+ '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
24
+ 'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
25
+ '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
26
+ '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
27
+ props.class,
28
+ )
29
+ "
30
+ @click="toggleSidebar"
31
+ >
32
+ <slot />
33
+ </button>
34
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import Separator from '@/components/ui/separator/Separator.vue';
3
+ import { cn } from '@/lib/utils';
4
+ import type { HTMLAttributes } from 'vue';
5
+
6
+ const props = defineProps<{
7
+ class?: HTMLAttributes['class'];
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <Separator data-sidebar="separator" :class="cn('mx-2 w-auto bg-sidebar-border', props.class)">
13
+ <slot />
14
+ </Separator>
15
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import Button from '@/components/ui/button/Button.vue';
3
+ import { cn } from '@/lib/utils';
4
+ import { PanelLeft } from 'lucide-vue-next';
5
+ import type { HTMLAttributes } from 'vue';
6
+ import { useSidebar } from './utils';
7
+
8
+ const props = defineProps<{
9
+ class?: HTMLAttributes['class'];
10
+ }>();
11
+
12
+ const { toggleSidebar } = useSidebar();
13
+ </script>
14
+
15
+ <template>
16
+ <Button data-sidebar="trigger" variant="ghost" size="icon" :class="cn('h-7 w-7', props.class)" @click="toggleSidebar">
17
+ <PanelLeft />
18
+ <span class="sr-only">Toggle Sidebar</span>
19
+ </Button>
20
+ </template>
@@ -0,0 +1,51 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+
3
+ export { default as Sidebar } from './Sidebar.vue';
4
+ export { default as SidebarContent } from './SidebarContent.vue';
5
+ export { default as SidebarFooter } from './SidebarFooter.vue';
6
+ export { default as SidebarGroup } from './SidebarGroup.vue';
7
+ export { default as SidebarGroupAction } from './SidebarGroupAction.vue';
8
+ export { default as SidebarGroupContent } from './SidebarGroupContent.vue';
9
+ export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue';
10
+ export { default as SidebarHeader } from './SidebarHeader.vue';
11
+ export { default as SidebarInput } from './SidebarInput.vue';
12
+ export { default as SidebarInset } from './SidebarInset.vue';
13
+ export { default as SidebarMenu } from './SidebarMenu.vue';
14
+ export { default as SidebarMenuAction } from './SidebarMenuAction.vue';
15
+ export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue';
16
+ export { default as SidebarMenuButton } from './SidebarMenuButton.vue';
17
+ export { default as SidebarMenuItem } from './SidebarMenuItem.vue';
18
+ export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue';
19
+ export { default as SidebarMenuSub } from './SidebarMenuSub.vue';
20
+ export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue';
21
+ export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue';
22
+ export { default as SidebarProvider } from './SidebarProvider.vue';
23
+ export { default as SidebarRail } from './SidebarRail.vue';
24
+ export { default as SidebarSeparator } from './SidebarSeparator.vue';
25
+ export { default as SidebarTrigger } from './SidebarTrigger.vue';
26
+
27
+ export { useSidebar } from './utils';
28
+
29
+ export const sidebarMenuButtonVariants = cva(
30
+ 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
31
+ {
32
+ variants: {
33
+ variant: {
34
+ default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
35
+ outline:
36
+ 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
37
+ },
38
+ size: {
39
+ default: 'h-8 text-sm',
40
+ sm: 'h-7 text-xs',
41
+ lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
42
+ },
43
+ },
44
+ defaultVariants: {
45
+ variant: 'default',
46
+ size: 'default',
47
+ },
48
+ },
49
+ );
50
+
51
+ export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>;
@@ -0,0 +1,19 @@
1
+ import { createContext } from 'radix-vue';
2
+ import type { ComputedRef, Ref } from 'vue';
3
+
4
+ export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
5
+ export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
6
+ export const SIDEBAR_WIDTH = '16rem';
7
+ export const SIDEBAR_WIDTH_MOBILE = '18rem';
8
+ export const SIDEBAR_WIDTH_ICON = '3rem';
9
+ export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
10
+
11
+ export const [useSidebar, provideSidebarContext] = createContext<{
12
+ state: ComputedRef<'expanded' | 'collapsed'>;
13
+ open: Ref<boolean>;
14
+ setOpen: (value: boolean) => void;
15
+ isMobile: Ref<boolean>;
16
+ openMobile: Ref<boolean>;
17
+ setOpenMobile: (value: boolean) => void;
18
+ toggleSidebar: () => void;
19
+ }>('Sidebar');
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import type { HTMLAttributes } from 'vue';
4
+
5
+ interface SkeletonProps {
6
+ class?: HTMLAttributes['class'];
7
+ }
8
+
9
+ const props = defineProps<SkeletonProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <div :class="cn('animate-pulse rounded-md bg-accent', props.class)" />
14
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Skeleton } from './Skeleton.vue';
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import { TooltipRoot, useForwardPropsEmits, type TooltipRootEmits, type TooltipRootProps } from 'radix-vue';
3
+
4
+ const props = defineProps<TooltipRootProps>();
5
+ const emits = defineEmits<TooltipRootEmits>();
6
+
7
+ const forwarded = useForwardPropsEmits(props, emits);
8
+ </script>
9
+
10
+ <template>
11
+ <TooltipRoot v-bind="forwarded">
12
+ <slot />
13
+ </TooltipRoot>
14
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import { TooltipContent, TooltipPortal, useForwardPropsEmits, type TooltipContentEmits, type TooltipContentProps } from 'radix-vue';
4
+ import { computed, type HTMLAttributes } from 'vue';
5
+
6
+ defineOptions({
7
+ inheritAttrs: false,
8
+ });
9
+
10
+ const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
11
+ sideOffset: 4,
12
+ });
13
+
14
+ const emits = defineEmits<TooltipContentEmits>();
15
+
16
+ const delegatedProps = computed(() => {
17
+ const { class: _, ...delegated } = props;
18
+
19
+ return delegated;
20
+ });
21
+
22
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
23
+ </script>
24
+
25
+ <template>
26
+ <TooltipPortal>
27
+ <TooltipContent
28
+ v-bind="{ ...forwarded, ...$attrs }"
29
+ :class="
30
+ cn(
31
+ 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
32
+ props.class,
33
+ )
34
+ "
35
+ >
36
+ <slot />
37
+ </TooltipContent>
38
+ </TooltipPortal>
39
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import { TooltipProvider, type TooltipProviderProps } from 'radix-vue';
3
+
4
+ const props = defineProps<TooltipProviderProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <TooltipProvider v-bind="props">
9
+ <slot />
10
+ </TooltipProvider>
11
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import { TooltipTrigger, type TooltipTriggerProps } from 'radix-vue';
3
+
4
+ const props = defineProps<TooltipTriggerProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <TooltipTrigger v-bind="props">
9
+ <slot />
10
+ </TooltipTrigger>
11
+ </template>
@@ -0,0 +1,4 @@
1
+ export { default as Tooltip } from './Tooltip.vue';
2
+ export { default as TooltipContent } from './TooltipContent.vue';
3
+ export { default as TooltipProvider } from './TooltipProvider.vue';
4
+ export { default as TooltipTrigger } from './TooltipTrigger.vue';
@@ -0,0 +1,53 @@
1
+ import { onMounted, ref } from 'vue';
2
+
3
+ type Appearance = 'light' | 'dark' | 'system';
4
+
5
+ export function updateTheme(value: Appearance) {
6
+ if (value === 'system') {
7
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
8
+ document.documentElement.classList.toggle('dark', systemTheme === 'dark');
9
+ } else {
10
+ document.documentElement.classList.toggle('dark', value === 'dark');
11
+ }
12
+ }
13
+
14
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
15
+
16
+ const handleSystemThemeChange = () => {
17
+ const currentAppearance = localStorage.getItem('appearance') as Appearance | null;
18
+ updateTheme(currentAppearance || 'system');
19
+ };
20
+
21
+ export function initializeTheme() {
22
+ // Initialize theme from saved preference or default to system...
23
+ const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
24
+ updateTheme(savedAppearance || 'system');
25
+
26
+ // Set up system theme change listener...
27
+ mediaQuery.addEventListener('change', handleSystemThemeChange);
28
+ }
29
+
30
+ export function useAppearance() {
31
+ const appearance = ref<Appearance>('system');
32
+
33
+ onMounted(() => {
34
+ initializeTheme();
35
+
36
+ const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
37
+
38
+ if (savedAppearance) {
39
+ appearance.value = savedAppearance;
40
+ }
41
+ });
42
+
43
+ function updateAppearance(value: Appearance) {
44
+ appearance.value = value;
45
+ localStorage.setItem('appearance', value);
46
+ updateTheme(value);
47
+ }
48
+
49
+ return {
50
+ appearance,
51
+ updateAppearance,
52
+ };
53
+ }
@@ -0,0 +1,14 @@
1
+ export function getInitials(fullName?: string): string {
2
+ if (!fullName) return '';
3
+
4
+ const names = fullName.trim().split(' ');
5
+
6
+ if (names.length === 0) return '';
7
+ if (names.length === 1) return names[0].charAt(0).toUpperCase();
8
+
9
+ return `${names[0].charAt(0)}${names[names.length - 1].charAt(0)}`.toUpperCase();
10
+ }
11
+
12
+ export function useInitials() {
13
+ return { getInitials };
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
2
+ import type { DefineComponent } from 'vue';
3
+ import { cmsPages } from './pages';
4
+
5
+ export { initializeTheme } from './composables/useAppearance';
6
+ export { cmsPages };
7
+
8
+ type PageGlob = Record<string, () => Promise<unknown>>;
9
+
10
+ /**
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`.
16
+ */
17
+ export function resolveCmsPage(name: string, localPages: PageGlob = {}) {
18
+ return resolvePageComponent(`./pages/${name}.vue`, {
19
+ ...cmsPages,
20
+ ...localPages,
21
+ } as Record<string, () => Promise<DefineComponent>>);
22
+ }
@@ -0,0 +1,170 @@
1
+ <script setup lang="ts">
2
+ import AppContent from '@/components/AppContent.vue';
3
+ import AppLogoIcon from '@/components/AppLogoIcon.vue';
4
+ import AppShell from '@/components/AppShell.vue';
5
+ import AuthBar from '@/components/AuthBar.vue';
6
+ import NavUser from '@/components/NavUser.vue';
7
+ import {
8
+ Sidebar,
9
+ SidebarContent,
10
+ SidebarFooter,
11
+ SidebarGroup,
12
+ SidebarGroupLabel,
13
+ SidebarHeader,
14
+ SidebarMenu,
15
+ SidebarMenuButton,
16
+ SidebarMenuItem,
17
+ SidebarTrigger,
18
+ } from '@/components/ui/sidebar';
19
+ import { Link, usePage } from '@inertiajs/vue3';
20
+ import {
21
+ FileStack,
22
+ FileText,
23
+ Folder,
24
+ Image,
25
+ Inbox,
26
+ LayoutGrid,
27
+ ListOrdered,
28
+ Menu,
29
+ Paintbrush,
30
+ Settings,
31
+ ShoppingBag,
32
+ Tag,
33
+ Users,
34
+ } from 'lucide-vue-next';
35
+ import { type Component, computed, ref, watch } from 'vue';
36
+
37
+ interface NavItem {
38
+ label: string;
39
+ href: string;
40
+ icon: Component;
41
+ permission?: string;
42
+ }
43
+
44
+ // Full CMS nav. Items with a `permission` are hidden unless the shared ability map allows them,
45
+ // so every user sees only what their role grants (super-admin sees all via the gate bypass).
46
+ const nav: NavItem[] = [
47
+ { label: 'Dashboard', href: '/dashboard', icon: LayoutGrid },
48
+ { label: 'Posts', href: '/admin/content/post', icon: FileText, permission: 'posts' },
49
+ { label: 'Pages', href: '/admin/content/page', icon: FileStack, permission: 'pages' },
50
+ { label: 'Categories', href: '/admin/taxonomy/category', icon: Folder, permission: 'taxonomy' },
51
+ { label: 'Tags', href: '/admin/taxonomy/post_tag', icon: Tag, permission: 'taxonomy' },
52
+ { label: 'Media', href: '/admin/media', icon: Image, permission: 'media' },
53
+ { label: 'Forms', href: '/admin/forms', icon: Inbox, permission: 'forms' },
54
+ { label: 'Products', href: '/admin/commerce/products', icon: ShoppingBag, permission: 'products' },
55
+ { label: 'Orders', href: '/admin/commerce/orders', icon: ListOrdered, permission: 'orders' },
56
+ { label: 'Menus', href: '/admin/menus', icon: Menu, permission: 'menus' },
57
+ { label: 'Users', href: '/admin/users', icon: Users, permission: 'users' },
58
+ { label: 'Theme', href: '/admin/appearance/theme', icon: Paintbrush, permission: 'theme' },
59
+ { label: 'Widgets', href: '/admin/appearance/widgets', icon: LayoutGrid, permission: 'widgets' },
60
+ { label: 'Settings', href: '/admin/settings', icon: Settings, permission: 'settings' },
61
+ ];
62
+
63
+ const page = usePage();
64
+ const url = computed(() => page.url);
65
+ const site = computed(
66
+ () =>
67
+ (page.props.site as { name: string; logo: string | null; showName: boolean } | undefined) ?? {
68
+ name: 'Dashboard',
69
+ logo: null,
70
+ showName: false,
71
+ },
72
+ );
73
+ // Name shows whenever there's no logo, or the logo is configured to sit beside the name.
74
+ const showName = computed(() => !site.value.logo || site.value.showName);
75
+ 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]));
77
+
78
+ const isActive = (href: string) => (href === '/dashboard' ? url.value === '/dashboard' : url.value.startsWith(href));
79
+
80
+ // Transient success toast driven by flash data.
81
+ const flashSuccess = computed(() => (page.props.flash as { success?: string } | undefined)?.success);
82
+ const showToast = ref(false);
83
+ let timer: ReturnType<typeof setTimeout> | undefined;
84
+
85
+ watch(
86
+ flashSuccess,
87
+ (value) => {
88
+ if (value && typeof window !== 'undefined') {
89
+ showToast.value = true;
90
+ clearTimeout(timer);
91
+ timer = setTimeout(() => (showToast.value = false), 3500);
92
+ }
93
+ },
94
+ { immediate: true },
95
+ );
96
+ </script>
97
+
98
+ <template>
99
+ <AppShell variant="sidebar">
100
+ <!-- Authenticated admin bar (WordPress-style): full-width row above the whole shell. -->
101
+ <template #topbar>
102
+ <AuthBar primary-label="View site" primary-href="/" primary-kind="site" />
103
+ </template>
104
+
105
+ <!-- Offset the fixed sidebar down by the 2rem bar so it sits below it, not under it. -->
106
+ <Sidebar collapsible="icon" variant="inset" class="top-8 h-[calc(100svh-2rem)]">
107
+ <SidebarHeader>
108
+ <SidebarMenu>
109
+ <SidebarMenuItem>
110
+ <SidebarMenuButton size="lg" as-child>
111
+ <Link href="/dashboard">
112
+ <!-- Icon square: the logo if uploaded, otherwise the lettermark. -->
113
+ <div
114
+ class="flex aspect-square size-8 items-center justify-center overflow-hidden rounded-md"
115
+ :class="site.logo ? 'bg-sidebar-accent' : 'bg-sidebar-primary text-sidebar-primary-foreground'"
116
+ >
117
+ <img v-if="site.logo" :src="site.logo" :alt="site.name" class="size-full object-contain" />
118
+ <AppLogoIcon v-else class="size-5 fill-current text-white dark:text-black" />
119
+ </div>
120
+ <!-- Name to the right: always without a logo; with a logo only when enabled. -->
121
+ <div v-if="showName" class="ml-1 grid flex-1 text-left text-sm">
122
+ <span class="truncate font-semibold leading-tight">{{ site.name }}</span>
123
+ </div>
124
+ </Link>
125
+ </SidebarMenuButton>
126
+ </SidebarMenuItem>
127
+ </SidebarMenu>
128
+ </SidebarHeader>
129
+
130
+ <SidebarContent>
131
+ <SidebarGroup class="px-2 py-0">
132
+ <SidebarGroupLabel>Manage</SidebarGroupLabel>
133
+ <SidebarMenu>
134
+ <SidebarMenuItem v-for="item in visibleNav" :key="item.href">
135
+ <SidebarMenuButton as-child :is-active="isActive(item.href)" :tooltip="item.label">
136
+ <Link :href="item.href">
137
+ <component :is="item.icon" />
138
+ <span>{{ item.label }}</span>
139
+ </Link>
140
+ </SidebarMenuButton>
141
+ </SidebarMenuItem>
142
+ </SidebarMenu>
143
+ </SidebarGroup>
144
+ </SidebarContent>
145
+
146
+ <SidebarFooter>
147
+ <NavUser />
148
+ </SidebarFooter>
149
+ </Sidebar>
150
+
151
+ <AppContent variant="sidebar" class="min-h-0 peer-data-[variant=inset]:min-h-0">
152
+ <header class="flex h-16 shrink-0 items-center gap-2 border-b border-sidebar-border/70 px-6">
153
+ <SidebarTrigger class="-ml-1 text-foreground" />
154
+ <h1 class="text-base font-medium text-foreground"><slot name="title">Dashboard</slot></h1>
155
+ </header>
156
+
157
+ <div class="flex-1 p-6">
158
+ <slot />
159
+ </div>
160
+ </AppContent>
161
+
162
+ <div
163
+ v-if="showToast"
164
+ class="fixed right-4 top-4 z-50 flex items-center gap-3 rounded-md bg-foreground px-4 py-2 text-sm text-background shadow-lg"
165
+ >
166
+ <span>{{ flashSuccess }}</span>
167
+ <button class="opacity-70 transition-opacity hover:opacity-100" @click="showToast = false">✕</button>
168
+ </div>
169
+ </AppShell>
170
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import AuthLayout from '@/layouts/auth/AuthSimpleLayout.vue';
3
+
4
+ defineProps<{
5
+ title?: string;
6
+ description?: string;
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <AuthLayout :title="title" :description="description">
12
+ <slot />
13
+ </AuthLayout>
14
+ </template>