@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.
- package/README.md +102 -0
- package/package.json +49 -0
- package/src/components/AppContent.vue +21 -0
- package/src/components/AppLogoIcon.vue +24 -0
- package/src/components/AppShell.vue +37 -0
- package/src/components/AppearanceTabs.vue +37 -0
- package/src/components/AuthBar.vue +58 -0
- package/src/components/BlockEditor.vue +95 -0
- package/src/components/DeleteUser.vue +87 -0
- package/src/components/FieldBuilder.vue +105 -0
- package/src/components/Heading.vue +20 -0
- package/src/components/HeadingSmall.vue +17 -0
- package/src/components/Icon.vue +30 -0
- package/src/components/InputError.vue +13 -0
- package/src/components/MenuItemsEditor.vue +59 -0
- package/src/components/NavUser.vue +30 -0
- package/src/components/Pagination.vue +28 -0
- package/src/components/PlaceholderPattern.vue +16 -0
- package/src/components/Seo.vue +28 -0
- package/src/components/TextLink.vue +24 -0
- package/src/components/UserInfo.vue +34 -0
- package/src/components/UserMenuContent.vue +37 -0
- package/src/components/commerce/OptionsEditor.vue +55 -0
- package/src/components/commerce/VariantsEditor.vue +71 -0
- package/src/components/ui/avatar/Avatar.vue +24 -0
- package/src/components/ui/avatar/AvatarFallback.vue +11 -0
- package/src/components/ui/avatar/AvatarImage.vue +9 -0
- package/src/components/ui/avatar/index.ts +24 -0
- package/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +18 -0
- package/src/components/ui/breadcrumb/BreadcrumbItem.vue +14 -0
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +15 -0
- package/src/components/ui/breadcrumb/BreadcrumbList.vue +14 -0
- package/src/components/ui/breadcrumb/BreadcrumbPage.vue +14 -0
- package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +17 -0
- package/src/components/ui/breadcrumb/index.ts +7 -0
- package/src/components/ui/button/Button.vue +22 -0
- package/src/components/ui/button/index.ts +31 -0
- package/src/components/ui/card/Card.vue +14 -0
- package/src/components/ui/card/CardContent.vue +14 -0
- package/src/components/ui/card/CardDescription.vue +14 -0
- package/src/components/ui/card/CardFooter.vue +14 -0
- package/src/components/ui/card/CardHeader.vue +14 -0
- package/src/components/ui/card/CardTitle.vue +14 -0
- package/src/components/ui/card/index.ts +6 -0
- package/src/components/ui/checkbox/Checkbox.vue +36 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/collapsible/Collapsible.vue +15 -0
- package/src/components/ui/collapsible/CollapsibleContent.vue +14 -0
- package/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
- package/src/components/ui/collapsible/index.ts +3 -0
- package/src/components/ui/dialog/Dialog.vue +14 -0
- package/src/components/ui/dialog/DialogClose.vue +11 -0
- package/src/components/ui/dialog/DialogContent.vue +51 -0
- package/src/components/ui/dialog/DialogDescription.vue +21 -0
- package/src/components/ui/dialog/DialogFooter.vue +12 -0
- package/src/components/ui/dialog/DialogHeader.vue +14 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/src/components/ui/dialog/DialogTitle.vue +21 -0
- package/src/components/ui/dialog/DialogTrigger.vue +11 -0
- package/src/components/ui/dialog/index.ts +9 -0
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +42 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +30 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +21 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +43 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +21 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
- package/src/components/ui/dropdown-menu/index.ts +16 -0
- package/src/components/ui/input/Input.vue +32 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/label/Label.vue +22 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/navigation-menu/NavigationMenu.vue +25 -0
- package/src/components/ui/navigation-menu/NavigationMenuContent.vue +31 -0
- package/src/components/ui/navigation-menu/NavigationMenuIndicator.vue +29 -0
- package/src/components/ui/navigation-menu/NavigationMenuItem.vue +11 -0
- package/src/components/ui/navigation-menu/NavigationMenuLink.vue +14 -0
- package/src/components/ui/navigation-menu/NavigationMenuList.vue +21 -0
- package/src/components/ui/navigation-menu/NavigationMenuTrigger.vue +24 -0
- package/src/components/ui/navigation-menu/NavigationMenuViewport.vue +29 -0
- package/src/components/ui/navigation-menu/index.ts +14 -0
- package/src/components/ui/separator/Separator.vue +31 -0
- package/src/components/ui/separator/index.ts +1 -0
- package/src/components/ui/sheet/Sheet.vue +14 -0
- package/src/components/ui/sheet/SheetClose.vue +11 -0
- package/src/components/ui/sheet/SheetContent.vue +53 -0
- package/src/components/ui/sheet/SheetDescription.vue +19 -0
- package/src/components/ui/sheet/SheetFooter.vue +12 -0
- package/src/components/ui/sheet/SheetHeader.vue +12 -0
- package/src/components/ui/sheet/SheetTitle.vue +19 -0
- package/src/components/ui/sheet/SheetTrigger.vue +11 -0
- package/src/components/ui/sheet/index.ts +29 -0
- package/src/components/ui/sidebar/Sidebar.vue +99 -0
- package/src/components/ui/sidebar/SidebarContent.vue +17 -0
- package/src/components/ui/sidebar/SidebarFooter.vue +14 -0
- package/src/components/ui/sidebar/SidebarGroup.vue +14 -0
- package/src/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/src/components/ui/sidebar/SidebarGroupContent.vue +14 -0
- package/src/components/ui/sidebar/SidebarGroupLabel.vue +29 -0
- package/src/components/ui/sidebar/SidebarHeader.vue +14 -0
- package/src/components/ui/sidebar/SidebarInput.vue +15 -0
- package/src/components/ui/sidebar/SidebarInset.vue +22 -0
- package/src/components/ui/sidebar/SidebarMenu.vue +14 -0
- package/src/components/ui/sidebar/SidebarMenuAction.vue +41 -0
- package/src/components/ui/sidebar/SidebarMenuBadge.vue +27 -0
- package/src/components/ui/sidebar/SidebarMenuButton.vue +52 -0
- package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +33 -0
- package/src/components/ui/sidebar/SidebarMenuItem.vue +14 -0
- package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +22 -0
- package/src/components/ui/sidebar/SidebarMenuSub.vue +23 -0
- package/src/components/ui/sidebar/SidebarMenuSubButton.vue +42 -0
- package/src/components/ui/sidebar/SidebarMenuSubItem.vue +7 -0
- package/src/components/ui/sidebar/SidebarProvider.vue +89 -0
- package/src/components/ui/sidebar/SidebarRail.vue +34 -0
- package/src/components/ui/sidebar/SidebarSeparator.vue +15 -0
- package/src/components/ui/sidebar/SidebarTrigger.vue +20 -0
- package/src/components/ui/sidebar/index.ts +51 -0
- package/src/components/ui/sidebar/utils.ts +19 -0
- package/src/components/ui/skeleton/Skeleton.vue +14 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/tooltip/Tooltip.vue +14 -0
- package/src/components/ui/tooltip/TooltipContent.vue +39 -0
- package/src/components/ui/tooltip/TooltipProvider.vue +11 -0
- package/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/composables/useAppearance.ts +53 -0
- package/src/composables/useInitials.ts +14 -0
- package/src/index.ts +22 -0
- package/src/layouts/AdminLayout.vue +170 -0
- package/src/layouts/AuthLayout.vue +14 -0
- package/src/layouts/PublicLayout.vue +53 -0
- package/src/layouts/auth/AuthCardLayout.vue +36 -0
- package/src/layouts/auth/AuthSimpleLayout.vue +31 -0
- package/src/layouts/auth/AuthSplitLayout.vue +40 -0
- package/src/layouts/settings/Layout.vue +56 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/Admin/Appearance/Theme.vue +58 -0
- package/src/pages/Admin/Appearance/Widgets.vue +48 -0
- package/src/pages/Admin/Commerce/Orders/Index.vue +80 -0
- package/src/pages/Admin/Commerce/Orders/Show.vue +200 -0
- package/src/pages/Admin/Commerce/Products/Edit.vue +167 -0
- package/src/pages/Admin/Commerce/Products/Index.vue +65 -0
- package/src/pages/Admin/Content/Edit.vue +170 -0
- package/src/pages/Admin/Content/Index.vue +88 -0
- package/src/pages/Admin/Content/Preview.vue +25 -0
- package/src/pages/Admin/Dashboard.vue +26 -0
- package/src/pages/Admin/Forms/Edit.vue +98 -0
- package/src/pages/Admin/Forms/Index.vue +68 -0
- package/src/pages/Admin/Forms/Submissions/Index.vue +68 -0
- package/src/pages/Admin/Forms/Submissions/Show.vue +47 -0
- package/src/pages/Admin/Media/Index.vue +75 -0
- package/src/pages/Admin/Menus/Create.vue +37 -0
- package/src/pages/Admin/Menus/Edit.vue +54 -0
- package/src/pages/Admin/Menus/Index.vue +52 -0
- package/src/pages/Admin/Settings/Index.vue +184 -0
- package/src/pages/Admin/Taxonomy/Edit.vue +83 -0
- package/src/pages/Admin/Taxonomy/Index.vue +68 -0
- package/src/pages/Admin/Users/Edit.vue +82 -0
- package/src/pages/Admin/Users/Index.vue +74 -0
- package/src/pages/Public/Cart/Index.vue +108 -0
- package/src/pages/Public/Checkout/Confirmation.vue +110 -0
- package/src/pages/Public/Checkout/Index.vue +174 -0
- package/src/pages/Public/Index.vue +54 -0
- package/src/pages/Public/Shop/Index.vue +39 -0
- package/src/pages/Public/Shop/Show.vue +46 -0
- package/src/pages/Public/Show.vue +41 -0
- package/src/pages/Setup/Complete.vue +53 -0
- package/src/pages/Setup/Index.vue +85 -0
- package/src/pages/Welcome.vue +787 -0
- package/src/pages/auth/ConfirmPassword.vue +53 -0
- package/src/pages/auth/ForgotPassword.vue +54 -0
- package/src/pages/auth/Login.vue +91 -0
- package/src/pages/auth/Register.vue +83 -0
- package/src/pages/auth/ResetPassword.vue +81 -0
- package/src/pages/auth/VerifyEmail.vue +36 -0
- package/src/pages/settings/Appearance.vue +23 -0
- package/src/pages/settings/Password.vue +120 -0
- package/src/pages/settings/Profile.vue +105 -0
- package/src/pages.ts +9 -0
- package/src/types/index.ts +42 -0
- 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>
|