@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,30 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import * as icons from 'lucide-vue-next';
4
+ import { computed } from 'vue';
5
+
6
+ interface Props {
7
+ name: string;
8
+ class?: string;
9
+ size?: number | string;
10
+ color?: string;
11
+ strokeWidth?: number | string;
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ class: '',
16
+ size: 16,
17
+ strokeWidth: 2,
18
+ });
19
+
20
+ const className = computed(() => cn('h-4 w-4', props.class));
21
+
22
+ const icon = computed(() => {
23
+ const iconName = props.name.charAt(0).toUpperCase() + props.name.slice(1);
24
+ return (icons as Record<string, any>)[iconName];
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <component :is="icon" :class="className" :size="size" :stroke-width="strokeWidth" :color="color" />
30
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ message?: string;
4
+ }>();
5
+ </script>
6
+
7
+ <template>
8
+ <div v-show="message">
9
+ <p class="text-sm text-red-600 dark:text-red-500">
10
+ {{ message }}
11
+ </p>
12
+ </div>
13
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { Button, Input } from '@tower_74/cms-ui';
3
+
4
+ interface Item {
5
+ label: string;
6
+ url: string;
7
+ }
8
+
9
+ const props = defineProps<{ modelValue: Item[] }>();
10
+ const emit = defineEmits<{ 'update:modelValue': [value: Item[]] }>();
11
+
12
+ const commit = (items: Item[]) => emit('update:modelValue', items);
13
+
14
+ const add = () => commit([...props.modelValue, { label: '', url: '' }]);
15
+
16
+ const remove = (index: number) => {
17
+ const items = [...props.modelValue];
18
+ items.splice(index, 1);
19
+ commit(items);
20
+ };
21
+
22
+ const move = (index: number, direction: -1 | 1) => {
23
+ const target = index + direction;
24
+ if (target < 0 || target >= props.modelValue.length) return;
25
+ const items = [...props.modelValue];
26
+ [items[index], items[target]] = [items[target], items[index]];
27
+ commit(items);
28
+ };
29
+
30
+ const setField = (index: number, key: 'label' | 'url', value: string) => {
31
+ const items = [...props.modelValue];
32
+ items[index] = { ...items[index], [key]: value };
33
+ commit(items);
34
+ };
35
+ </script>
36
+
37
+ <template>
38
+ <div class="space-y-3">
39
+ <p v-if="!modelValue.length" class="rounded border border-dashed border-border px-4 py-6 text-center text-sm text-muted">
40
+ No items yet. Add your first link below.
41
+ </p>
42
+
43
+ <div v-for="(item, index) in modelValue" :key="index" class="flex items-end gap-2 rounded-lg border border-border bg-background p-3">
44
+ <div class="flex-1">
45
+ <label class="mb-1 block text-xs font-medium text-muted">Label</label>
46
+ <Input :model-value="item.label" placeholder="Home" @update:model-value="setField(index, 'label', $event)" />
47
+ </div>
48
+ <div class="flex-1">
49
+ <label class="mb-1 block text-xs font-medium text-muted">URL</label>
50
+ <Input :model-value="item.url" placeholder="/ or https://…" @update:model-value="setField(index, 'url', $event)" />
51
+ </div>
52
+ <Button type="button" variant="ghost" size="sm" :disabled="index === 0" @click="move(index, -1)">↑</Button>
53
+ <Button type="button" variant="ghost" size="sm" :disabled="index === modelValue.length - 1" @click="move(index, 1)">↓</Button>
54
+ <Button type="button" variant="danger" size="sm" @click="remove(index)">✕</Button>
55
+ </div>
56
+
57
+ <Button type="button" variant="secondary" @click="add">Add item</Button>
58
+ </div>
59
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import UserInfo from '@/components/UserInfo.vue';
3
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
4
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
5
+ import { type SharedData, type User } from '@/types';
6
+ import { usePage } from '@inertiajs/vue3';
7
+ import { ChevronsUpDown } from 'lucide-vue-next';
8
+ import UserMenuContent from './UserMenuContent.vue';
9
+
10
+ const page = usePage<SharedData>();
11
+ const user = page.props.auth.user as User;
12
+ </script>
13
+
14
+ <template>
15
+ <SidebarMenu>
16
+ <SidebarMenuItem>
17
+ <DropdownMenu>
18
+ <DropdownMenuTrigger as-child>
19
+ <SidebarMenuButton size="lg" class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
20
+ <UserInfo :user="user" />
21
+ <ChevronsUpDown class="ml-auto size-4" />
22
+ </SidebarMenuButton>
23
+ </DropdownMenuTrigger>
24
+ <DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom" align="end" :side-offset="4">
25
+ <UserMenuContent :user="user" />
26
+ </DropdownMenuContent>
27
+ </DropdownMenu>
28
+ </SidebarMenuItem>
29
+ </SidebarMenu>
30
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { Link } from '@inertiajs/vue3';
3
+
4
+ interface PageLink {
5
+ url: string | null;
6
+ label: string;
7
+ active: boolean;
8
+ }
9
+
10
+ defineProps<{ links: PageLink[] }>();
11
+ </script>
12
+
13
+ <template>
14
+ <!-- Laravel paginator links: [prev, …pages, next]. Hide when there's a single page. -->
15
+ <nav v-if="links.length > 3" class="mt-4 flex flex-wrap gap-1">
16
+ <template v-for="(link, i) in links" :key="i">
17
+ <span v-if="!link.url" class="px-3 py-1 text-sm text-muted" v-html="link.label" />
18
+ <Link
19
+ v-else
20
+ :href="link.url"
21
+ class="rounded px-3 py-1 text-sm transition-colors"
22
+ :class="link.active ? 'bg-primary text-primary-foreground' : 'text-text hover:bg-surface'"
23
+ >
24
+ <span v-html="link.label" />
25
+ </Link>
26
+ </template>
27
+ </nav>
28
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+
4
+ const patternId = computed(() => `pattern-${Math.random().toString(36).substring(2, 9)}`);
5
+ </script>
6
+
7
+ <template>
8
+ <svg class="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" fill="none">
9
+ <defs>
10
+ <pattern :id="patternId" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
11
+ <path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
12
+ </pattern>
13
+ </defs>
14
+ <rect stroke="none" :fill="`url(#${patternId})`" width="100%" height="100%"></rect>
15
+ </svg>
16
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { Head } from '@inertiajs/vue3';
3
+
4
+ defineProps<{
5
+ seo: {
6
+ title: string;
7
+ description?: string | null;
8
+ url?: string | null;
9
+ image?: string | null;
10
+ type?: string | null;
11
+ };
12
+ }>();
13
+ </script>
14
+
15
+ <template>
16
+ <Head :title="seo.title">
17
+ <meta v-if="seo.description" head-key="description" name="description" :content="seo.description" />
18
+ <link v-if="seo.url" head-key="canonical" rel="canonical" :href="seo.url" />
19
+
20
+ <meta head-key="og:title" property="og:title" :content="seo.title" />
21
+ <meta v-if="seo.description" head-key="og:description" property="og:description" :content="seo.description" />
22
+ <meta head-key="og:type" property="og:type" :content="seo.type ?? 'website'" />
23
+ <meta v-if="seo.url" head-key="og:url" property="og:url" :content="seo.url" />
24
+ <meta v-if="seo.image" head-key="og:image" property="og:image" :content="seo.image" />
25
+
26
+ <meta head-key="twitter:card" name="twitter:card" :content="seo.image ? 'summary_large_image' : 'summary'" />
27
+ </Head>
28
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { Link } from '@inertiajs/vue3';
3
+
4
+ interface Props {
5
+ href: string;
6
+ tabindex?: number;
7
+ method?: string;
8
+ as?: string;
9
+ }
10
+
11
+ defineProps<Props>();
12
+ </script>
13
+
14
+ <template>
15
+ <Link
16
+ :href="href"
17
+ :tabindex="tabindex"
18
+ :method="method"
19
+ :as="as"
20
+ class="hover:decoration-current! text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out dark:decoration-neutral-500"
21
+ >
22
+ <slot />
23
+ </Link>
24
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
3
+ import { useInitials } from '@/composables/useInitials';
4
+ import type { User } from '@/types';
5
+ import { computed } from 'vue';
6
+
7
+ interface Props {
8
+ user: User;
9
+ showEmail?: boolean;
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ showEmail: false,
14
+ });
15
+
16
+ const { getInitials } = useInitials();
17
+
18
+ // Compute whether we should show the avatar image
19
+ const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '');
20
+ </script>
21
+
22
+ <template>
23
+ <Avatar class="h-8 w-8 overflow-hidden rounded-lg">
24
+ <AvatarImage v-if="showAvatar" :src="user.avatar" :alt="user.name" />
25
+ <AvatarFallback class="rounded-lg text-black dark:text-white">
26
+ {{ getInitials(user.name) }}
27
+ </AvatarFallback>
28
+ </Avatar>
29
+
30
+ <div class="grid flex-1 text-left text-sm leading-tight">
31
+ <span class="truncate font-medium">{{ user.name }}</span>
32
+ <span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
33
+ </div>
34
+ </template>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import UserInfo from '@/components/UserInfo.vue';
3
+ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
4
+ import type { User } from '@/types';
5
+ import { Link } from '@inertiajs/vue3';
6
+ import { LogOut, Settings } from 'lucide-vue-next';
7
+
8
+ interface Props {
9
+ user: User;
10
+ }
11
+
12
+ defineProps<Props>();
13
+ </script>
14
+
15
+ <template>
16
+ <DropdownMenuLabel class="p-0 font-normal">
17
+ <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
18
+ <UserInfo :user="user" :show-email="true" />
19
+ </div>
20
+ </DropdownMenuLabel>
21
+ <DropdownMenuSeparator />
22
+ <DropdownMenuGroup>
23
+ <DropdownMenuItem :as-child="true">
24
+ <Link class="block w-full" :href="route('profile.edit')" as="button">
25
+ <Settings class="mr-2 h-4 w-4" />
26
+ Settings
27
+ </Link>
28
+ </DropdownMenuItem>
29
+ </DropdownMenuGroup>
30
+ <DropdownMenuSeparator />
31
+ <DropdownMenuItem :as-child="true">
32
+ <Link class="block w-full" method="post" :href="route('logout')" as="button">
33
+ <LogOut class="mr-2 h-4 w-4" />
34
+ Log out
35
+ </Link>
36
+ </DropdownMenuItem>
37
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import { Button, Input } from '@tower_74/cms-ui';
3
+
4
+ interface Option {
5
+ name: string;
6
+ values: string[];
7
+ }
8
+
9
+ const props = defineProps<{ modelValue: Option[] }>();
10
+ const emit = defineEmits<{ 'update:modelValue': [value: Option[]] }>();
11
+
12
+ const commit = (options: Option[]) => emit('update:modelValue', options);
13
+
14
+ const add = () => commit([...props.modelValue, { name: '', values: [] }]);
15
+ const remove = (i: number) => {
16
+ const options = [...props.modelValue];
17
+ options.splice(i, 1);
18
+ commit(options);
19
+ };
20
+ const setName = (i: number, name: string) => {
21
+ const options = [...props.modelValue];
22
+ options[i] = { ...options[i], name };
23
+ commit(options);
24
+ };
25
+ const setValues = (i: number, csv: string) => {
26
+ const options = [...props.modelValue];
27
+ options[i] = {
28
+ ...options[i],
29
+ values: csv
30
+ .split(',')
31
+ .map((s) => s.trim())
32
+ .filter(Boolean),
33
+ };
34
+ commit(options);
35
+ };
36
+ const valuesText = (option: Option) => option.values.join(', ');
37
+ </script>
38
+
39
+ <template>
40
+ <div class="space-y-3">
41
+ <div v-for="(option, i) in modelValue" :key="i" class="flex items-end gap-2 rounded-lg border border-border bg-background p-3">
42
+ <div class="w-48">
43
+ <label class="mb-1 block text-xs font-medium text-muted">Option</label>
44
+ <Input :model-value="option.name" placeholder="Size" @update:model-value="setName(i, $event)" />
45
+ </div>
46
+ <div class="flex-1">
47
+ <label class="mb-1 block text-xs font-medium text-muted">Values (comma-separated)</label>
48
+ <Input :model-value="valuesText(option)" placeholder="S, M, L" @update:model-value="setValues(i, $event)" />
49
+ </div>
50
+ <Button type="button" variant="danger" size="sm" @click="remove(i)">✕</Button>
51
+ </div>
52
+
53
+ <Button type="button" variant="secondary" @click="add">Add option</Button>
54
+ </div>
55
+ </template>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ import { Button, Input, Select } from '@tower_74/cms-ui';
3
+
4
+ interface Option {
5
+ name: string;
6
+ values: string[];
7
+ }
8
+ interface Variant {
9
+ sku: string;
10
+ price: string;
11
+ stock: number | string;
12
+ options: Record<string, string>;
13
+ }
14
+
15
+ const props = defineProps<{ modelValue: Variant[]; options: Option[]; currencySymbol: string }>();
16
+ const emit = defineEmits<{ 'update:modelValue': [value: Variant[]] }>();
17
+
18
+ const commit = (variants: Variant[]) => emit('update:modelValue', variants);
19
+
20
+ const add = () => {
21
+ const options: Record<string, string> = {};
22
+ props.options.forEach((o) => (options[o.name] = o.values[0] ?? ''));
23
+ commit([...props.modelValue, { sku: '', price: '', stock: 0, options }]);
24
+ };
25
+ const remove = (i: number) => {
26
+ const variants = [...props.modelValue];
27
+ variants.splice(i, 1);
28
+ commit(variants);
29
+ };
30
+ const setField = (i: number, key: 'sku' | 'price' | 'stock', value: string) => {
31
+ const variants = [...props.modelValue];
32
+ variants[i] = { ...variants[i], [key]: value };
33
+ commit(variants);
34
+ };
35
+ const setOption = (i: number, name: string, value: string) => {
36
+ const variants = [...props.modelValue];
37
+ variants[i] = { ...variants[i], options: { ...variants[i].options, [name]: value } };
38
+ commit(variants);
39
+ };
40
+ const optionsFor = (option: Option) => option.values.map((v) => ({ label: v, value: v }));
41
+ </script>
42
+
43
+ <template>
44
+ <div class="space-y-3">
45
+ <div v-for="(variant, i) in modelValue" :key="i" class="flex flex-wrap items-end gap-2 rounded-lg border border-border bg-background p-3">
46
+ <div v-for="option in options" :key="option.name" class="w-32">
47
+ <label class="mb-1 block text-xs font-medium text-muted">{{ option.name }}</label>
48
+ <Select
49
+ :model-value="variant.options[option.name] ?? ''"
50
+ :options="optionsFor(option)"
51
+ @update:model-value="setOption(i, option.name, $event)"
52
+ />
53
+ </div>
54
+ <div class="w-40">
55
+ <label class="mb-1 block text-xs font-medium text-muted">SKU</label>
56
+ <Input :model-value="variant.sku" placeholder="ABC-001" @update:model-value="setField(i, 'sku', $event)" />
57
+ </div>
58
+ <div class="w-28">
59
+ <label class="mb-1 block text-xs font-medium text-muted">Price ({{ currencySymbol }})</label>
60
+ <Input :model-value="String(variant.price)" type="number" @update:model-value="setField(i, 'price', $event)" />
61
+ </div>
62
+ <div class="w-24">
63
+ <label class="mb-1 block text-xs font-medium text-muted">Stock</label>
64
+ <Input :model-value="String(variant.stock)" type="number" @update:model-value="setField(i, 'stock', $event)" />
65
+ </div>
66
+ <Button type="button" variant="danger" size="sm" @click="remove(i)">✕</Button>
67
+ </div>
68
+
69
+ <Button type="button" variant="secondary" @click="add">Add variant</Button>
70
+ </div>
71
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import { AvatarRoot } from 'radix-vue';
4
+ import type { HTMLAttributes } from 'vue';
5
+ import { avatarVariant, type AvatarVariants } from '.';
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ class?: HTMLAttributes['class'];
10
+ size?: AvatarVariants['size'];
11
+ shape?: AvatarVariants['shape'];
12
+ }>(),
13
+ {
14
+ size: 'sm',
15
+ shape: 'circle',
16
+ },
17
+ );
18
+ </script>
19
+
20
+ <template>
21
+ <AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
22
+ <slot />
23
+ </AvatarRoot>
24
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue';
3
+
4
+ const props = defineProps<AvatarFallbackProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <AvatarFallback v-bind="props">
9
+ <slot />
10
+ </AvatarFallback>
11
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ import { AvatarImage, type AvatarImageProps } from 'radix-vue';
3
+
4
+ const props = defineProps<AvatarImageProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <AvatarImage v-bind="props" class="h-full w-full object-cover" />
9
+ </template>
@@ -0,0 +1,24 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+
3
+ export { default as Avatar } from './Avatar.vue';
4
+ export { default as AvatarFallback } from './AvatarFallback.vue';
5
+ export { default as AvatarImage } from './AvatarImage.vue';
6
+
7
+ export const avatarVariant = cva(
8
+ 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
9
+ {
10
+ variants: {
11
+ size: {
12
+ sm: 'h-10 w-10 text-xs',
13
+ base: 'h-16 w-16 text-2xl',
14
+ lg: 'h-32 w-32 text-5xl',
15
+ },
16
+ shape: {
17
+ circle: 'rounded-full',
18
+ square: 'rounded-md',
19
+ },
20
+ },
21
+ },
22
+ );
23
+
24
+ export type AvatarVariants = VariantProps<typeof avatarVariant>;
@@ -0,0 +1,13 @@
1
+ <script lang="ts" setup>
2
+ import type { HTMLAttributes } from 'vue';
3
+
4
+ const props = defineProps<{
5
+ class?: HTMLAttributes['class'];
6
+ }>();
7
+ </script>
8
+
9
+ <template>
10
+ <nav aria-label="breadcrumb" :class="props.class">
11
+ <slot />
12
+ </nav>
13
+ </template>
@@ -0,0 +1,18 @@
1
+ <script lang="ts" setup>
2
+ import { cn } from '@/lib/utils';
3
+ import { MoreHorizontal } from 'lucide-vue-next';
4
+ import type { HTMLAttributes } from 'vue';
5
+
6
+ const props = defineProps<{
7
+ class?: HTMLAttributes['class'];
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <span role="presentation" aria-hidden="true" :class="cn('flex h-9 w-9 items-center justify-center', props.class)">
13
+ <slot>
14
+ <MoreHorizontal class="h-4 w-4" />
15
+ </slot>
16
+ <span class="sr-only">More</span>
17
+ </span>
18
+ </template>
@@ -0,0 +1,14 @@
1
+ <script lang="ts" setup>
2
+ import { cn } from '@/lib/utils';
3
+ import type { HTMLAttributes } from 'vue';
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class'];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <li :class="cn('inline-flex items-center gap-1.5', props.class)">
12
+ <slot />
13
+ </li>
14
+ </template>
@@ -0,0 +1,15 @@
1
+ <script lang="ts" setup>
2
+ import { cn } from '@/lib/utils';
3
+ import { Primitive, type PrimitiveProps } from 'radix-vue';
4
+ import type { HTMLAttributes } from 'vue';
5
+
6
+ const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
7
+ as: 'a',
8
+ });
9
+ </script>
10
+
11
+ <template>
12
+ <Primitive :as="as" :as-child="asChild" :class="cn('transition-colors hover:text-foreground', props.class)">
13
+ <slot />
14
+ </Primitive>
15
+ </template>
@@ -0,0 +1,14 @@
1
+ <script lang="ts" setup>
2
+ import { cn } from '@/lib/utils';
3
+ import type { HTMLAttributes } from 'vue';
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class'];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <ol :class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)">
12
+ <slot />
13
+ </ol>
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script lang="ts" setup>
2
+ import { cn } from '@/lib/utils';
3
+ import type { HTMLAttributes } from 'vue';
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class'];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <span role="link" aria-disabled="true" aria-current="page" :class="cn('font-normal text-foreground', props.class)">
12
+ <slot />
13
+ </span>
14
+ </template>
@@ -0,0 +1,17 @@
1
+ <script lang="ts" setup>
2
+ import { cn } from '@/lib/utils';
3
+ import { ChevronRight } from 'lucide-vue-next';
4
+ import type { HTMLAttributes } from 'vue';
5
+
6
+ const props = defineProps<{
7
+ class?: HTMLAttributes['class'];
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <li role="presentation" aria-hidden="true" :class="cn('[&>svg]:h-3.5 [&>svg]:w-3.5', props.class)">
13
+ <slot>
14
+ <ChevronRight />
15
+ </slot>
16
+ </li>
17
+ </template>
@@ -0,0 +1,7 @@
1
+ export { default as Breadcrumb } from './Breadcrumb.vue';
2
+ export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';
3
+ export { default as BreadcrumbItem } from './BreadcrumbItem.vue';
4
+ export { default as BreadcrumbLink } from './BreadcrumbLink.vue';
5
+ export { default as BreadcrumbList } from './BreadcrumbList.vue';
6
+ export { default as BreadcrumbPage } from './BreadcrumbPage.vue';
7
+ export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '@/lib/utils';
3
+ import { Primitive, type PrimitiveProps } from 'radix-vue';
4
+ import type { HTMLAttributes } from 'vue';
5
+ import { buttonVariants, type ButtonVariants } from '.';
6
+
7
+ interface Props extends PrimitiveProps {
8
+ variant?: ButtonVariants['variant'];
9
+ size?: ButtonVariants['size'];
10
+ class?: HTMLAttributes['class'];
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ as: 'button',
15
+ });
16
+ </script>
17
+
18
+ <template>
19
+ <Primitive :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
20
+ <slot />
21
+ </Primitive>
22
+ </template>