@plutocms/supabase 0.0.1-alpha.1
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/.agents/skills/nuxt/GENERATION.md +5 -0
- package/.agents/skills/nuxt/SKILL.md +55 -0
- package/.agents/skills/nuxt/references/advanced-hooks.md +289 -0
- package/.agents/skills/nuxt/references/advanced-layers.md +299 -0
- package/.agents/skills/nuxt/references/advanced-module-authoring.md +554 -0
- package/.agents/skills/nuxt/references/best-practices-data-fetching.md +357 -0
- package/.agents/skills/nuxt/references/best-practices-ssr.md +355 -0
- package/.agents/skills/nuxt/references/core-cli.md +263 -0
- package/.agents/skills/nuxt/references/core-config.md +162 -0
- package/.agents/skills/nuxt/references/core-data-fetching.md +236 -0
- package/.agents/skills/nuxt/references/core-deployment.md +224 -0
- package/.agents/skills/nuxt/references/core-directory-structure.md +269 -0
- package/.agents/skills/nuxt/references/core-modules.md +292 -0
- package/.agents/skills/nuxt/references/core-routing.md +226 -0
- package/.agents/skills/nuxt/references/features-components-autoimport.md +328 -0
- package/.agents/skills/nuxt/references/features-components.md +264 -0
- package/.agents/skills/nuxt/references/features-composables.md +276 -0
- package/.agents/skills/nuxt/references/features-server.md +265 -0
- package/.agents/skills/nuxt/references/features-state.md +194 -0
- package/.agents/skills/nuxt/references/rendering-modes.md +237 -0
- package/.agents/skills/nuxt-ui/SKILL.md +334 -0
- package/.agents/skills/nuxt-ui/references/components.md +377 -0
- package/.agents/skills/nuxt-ui/references/composables.md +127 -0
- package/.agents/skills/nuxt-ui/references/layouts/chat.md +195 -0
- package/.agents/skills/nuxt-ui/references/layouts/dashboard.md +220 -0
- package/.agents/skills/nuxt-ui/references/layouts/docs.md +141 -0
- package/.agents/skills/nuxt-ui/references/layouts/editor.md +167 -0
- package/.agents/skills/nuxt-ui/references/layouts/page.md +260 -0
- package/.agents/skills/nuxt-ui/references/theming.md +427 -0
- package/.agents/skills/supabase-postgres-best-practices/AGENTS.md +68 -0
- package/.agents/skills/supabase-postgres-best-practices/CLAUDE.md +1 -0
- package/.agents/skills/supabase-postgres-best-practices/README.md +116 -0
- package/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +57 -0
- package/.editorconfig +12 -0
- package/.env.example +2 -0
- package/.eslintignore +1 -0
- package/.prettierrc +9 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +61 -0
- package/README.md +62 -0
- package/app/components/navbar/NavbarAdmin.vue +169 -0
- package/app/composables/auth.ts +81 -0
- package/app/layouts/setup.vue +13 -0
- package/app/middleware/auth.ts +27 -0
- package/app/middleware/setup-check.ts +25 -0
- package/app/pages/admin/(auth)/login.vue +84 -0
- package/app/pages/admin/(auth)/signup.vue +128 -0
- package/app/pages/admin/settings.vue +116 -0
- package/app/pages/admin/setup.vue +300 -0
- package/app/pages/admin.vue +38 -0
- package/bun.lock +2685 -0
- package/eslint.config.mjs +88 -0
- package/nuxt.config.ts +55 -0
- package/package.json +44 -0
- package/public/schema.sql +103 -0
- package/scripts/supabase-typegen.ts +36 -0
- package/server/api/category/list.get.ts +9 -0
- package/server/api/category/new.post.ts +31 -0
- package/server/api/settings/first_setup.get.ts +39 -0
- package/server/api/settings/index.get.ts +27 -0
- package/server/api/settings/update.post.ts +36 -0
- package/server/api/setup/create.post.ts +58 -0
- package/server/api/signup.post.ts +28 -0
- package/server/api/users/[id].get.ts +21 -0
- package/server/api/users/index.get.ts +16 -0
- package/shared/types/supabase.ts +406 -0
- package/skills-lock.json +20 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Dashboard Layout
|
|
2
|
+
|
|
3
|
+
Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars.
|
|
4
|
+
|
|
5
|
+
## Component tree
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
UApp
|
|
9
|
+
└── NuxtLayout (dashboard)
|
|
10
|
+
└── UDashboardGroup
|
|
11
|
+
├── UDashboardSidebar
|
|
12
|
+
│ ├── #header (logo, search button)
|
|
13
|
+
│ ├── #default (navigation) — receives { collapsed } slot prop
|
|
14
|
+
│ └── #footer (user menu)
|
|
15
|
+
└── NuxtPage
|
|
16
|
+
└── UDashboardPanel
|
|
17
|
+
├── #header → UDashboardNavbar + UDashboardToolbar
|
|
18
|
+
├── #body (scrollable content)
|
|
19
|
+
└── #footer (optional)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Layout
|
|
23
|
+
|
|
24
|
+
```vue [layouts/dashboard.vue]
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import type { NavigationMenuItem } from '@nuxt/ui'
|
|
27
|
+
|
|
28
|
+
const items = computed<NavigationMenuItem[]>(() => [{
|
|
29
|
+
label: 'Home',
|
|
30
|
+
icon: 'i-lucide-house',
|
|
31
|
+
to: '/dashboard'
|
|
32
|
+
}, {
|
|
33
|
+
label: 'Inbox',
|
|
34
|
+
icon: 'i-lucide-inbox',
|
|
35
|
+
to: '/dashboard/inbox'
|
|
36
|
+
}, {
|
|
37
|
+
label: 'Users',
|
|
38
|
+
icon: 'i-lucide-users',
|
|
39
|
+
to: '/dashboard/users'
|
|
40
|
+
}, {
|
|
41
|
+
label: 'Settings',
|
|
42
|
+
icon: 'i-lucide-settings',
|
|
43
|
+
to: '/dashboard/settings'
|
|
44
|
+
}])
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<UDashboardGroup>
|
|
49
|
+
<UDashboardSidebar collapsible resizable>
|
|
50
|
+
<template #header="{ collapsed }">
|
|
51
|
+
<UDashboardSearchButton :collapsed="collapsed" />
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<template #default="{ collapsed }">
|
|
55
|
+
<UNavigationMenu
|
|
56
|
+
:items="items"
|
|
57
|
+
orientation="vertical"
|
|
58
|
+
:ui="{ link: collapsed ? 'justify-center' : undefined }"
|
|
59
|
+
/>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<template #footer="{ collapsed }">
|
|
63
|
+
<UButton
|
|
64
|
+
:icon="collapsed ? 'i-lucide-log-out' : undefined"
|
|
65
|
+
:label="collapsed ? undefined : 'Sign out'"
|
|
66
|
+
color="neutral"
|
|
67
|
+
variant="ghost"
|
|
68
|
+
block
|
|
69
|
+
/>
|
|
70
|
+
</template>
|
|
71
|
+
</UDashboardSidebar>
|
|
72
|
+
|
|
73
|
+
<slot />
|
|
74
|
+
</UDashboardGroup>
|
|
75
|
+
</template>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Page
|
|
79
|
+
|
|
80
|
+
```vue [pages/dashboard/index.vue]
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
definePageMeta({ layout: 'dashboard' })
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<UDashboardPanel>
|
|
87
|
+
<template #header>
|
|
88
|
+
<UDashboardNavbar title="Home">
|
|
89
|
+
<template #right>
|
|
90
|
+
<UButton icon="i-lucide-plus" label="New" />
|
|
91
|
+
</template>
|
|
92
|
+
</UDashboardNavbar>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<template #body>
|
|
96
|
+
<!-- Page content -->
|
|
97
|
+
</template>
|
|
98
|
+
</UDashboardPanel>
|
|
99
|
+
</template>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Key components
|
|
103
|
+
|
|
104
|
+
### DashboardGroup
|
|
105
|
+
|
|
106
|
+
Root layout wrapper. Manages sidebar state and persistence.
|
|
107
|
+
|
|
108
|
+
| Prop | Default | Description |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `storage` | `'cookie'` | State persistence: `'cookie'`, `'localStorage'`, `false` |
|
|
111
|
+
| `storage-key` | `'dashboard'` | Storage key name |
|
|
112
|
+
| `unit` | `'percentages'` | Size unit: `'percentages'` or `'pixels'` |
|
|
113
|
+
|
|
114
|
+
### DashboardSidebar
|
|
115
|
+
|
|
116
|
+
Resizable, collapsible sidebar. Must be inside `DashboardGroup`.
|
|
117
|
+
|
|
118
|
+
| Prop | Default | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `resizable` | `false` | Enable resize by dragging |
|
|
121
|
+
| `collapsible` | `false` | Enable collapse when dragged to edge |
|
|
122
|
+
| `side` | `'left'` | `'left'` or `'right'` |
|
|
123
|
+
| `mode` | `'slideover'` | Mobile menu mode: `'modal'`, `'slideover'`, `'drawer'` |
|
|
124
|
+
|
|
125
|
+
Slots receive `{ collapsed }` prop. Control state: `v-model:collapsed`, `v-model:open` (mobile).
|
|
126
|
+
|
|
127
|
+
### DashboardPanel
|
|
128
|
+
|
|
129
|
+
Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw) slots.
|
|
130
|
+
|
|
131
|
+
| Prop | Default | Description |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `id` | `—` | Unique ID (required for multi-panel) |
|
|
134
|
+
| `resizable` | `false` | Enable resize by dragging |
|
|
135
|
+
|
|
136
|
+
### DashboardNavbar / DashboardToolbar
|
|
137
|
+
|
|
138
|
+
Navbar has `#left`, `#default`, `#right` slots and a `title` prop. Toolbar has the same slots for filters/actions below the navbar.
|
|
139
|
+
|
|
140
|
+
## Multi-panel (list-detail)
|
|
141
|
+
|
|
142
|
+
```vue [pages/dashboard/inbox.vue]
|
|
143
|
+
<script setup lang="ts">
|
|
144
|
+
definePageMeta({ layout: 'dashboard' })
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<template>
|
|
148
|
+
<UDashboardPanel id="inbox-list" resizable>
|
|
149
|
+
<template #header>
|
|
150
|
+
<UDashboardNavbar title="Inbox" />
|
|
151
|
+
</template>
|
|
152
|
+
<template #body>
|
|
153
|
+
<!-- Email list -->
|
|
154
|
+
</template>
|
|
155
|
+
</UDashboardPanel>
|
|
156
|
+
|
|
157
|
+
<UDashboardPanel id="inbox-detail" class="hidden lg:flex">
|
|
158
|
+
<template #header>
|
|
159
|
+
<UDashboardNavbar title="Message" />
|
|
160
|
+
</template>
|
|
161
|
+
<template #body>
|
|
162
|
+
<!-- Email content -->
|
|
163
|
+
</template>
|
|
164
|
+
</UDashboardPanel>
|
|
165
|
+
</template>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## With toolbar
|
|
169
|
+
|
|
170
|
+
```vue
|
|
171
|
+
<UDashboardPanel>
|
|
172
|
+
<template #header>
|
|
173
|
+
<UDashboardNavbar title="Users" />
|
|
174
|
+
|
|
175
|
+
<UDashboardToolbar>
|
|
176
|
+
<template #left>
|
|
177
|
+
<UInput icon="i-lucide-search" placeholder="Search..." />
|
|
178
|
+
</template>
|
|
179
|
+
<template #right>
|
|
180
|
+
<USelect :items="['All', 'Active', 'Inactive']" />
|
|
181
|
+
</template>
|
|
182
|
+
</UDashboardToolbar>
|
|
183
|
+
</template>
|
|
184
|
+
</UDashboardPanel>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## With search
|
|
188
|
+
|
|
189
|
+
```vue [layouts/dashboard.vue]
|
|
190
|
+
<template>
|
|
191
|
+
<UDashboardGroup>
|
|
192
|
+
<UDashboardSidebar>
|
|
193
|
+
<template #header>
|
|
194
|
+
<UDashboardSearchButton />
|
|
195
|
+
</template>
|
|
196
|
+
<!-- ... -->
|
|
197
|
+
</UDashboardSidebar>
|
|
198
|
+
|
|
199
|
+
<slot />
|
|
200
|
+
|
|
201
|
+
<UDashboardSearch :groups="searchGroups" />
|
|
202
|
+
</UDashboardGroup>
|
|
203
|
+
</template>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Right sidebar
|
|
207
|
+
|
|
208
|
+
```vue
|
|
209
|
+
<UDashboardGroup>
|
|
210
|
+
<UDashboardSidebar collapsible resizable>
|
|
211
|
+
<!-- Left sidebar -->
|
|
212
|
+
</UDashboardSidebar>
|
|
213
|
+
|
|
214
|
+
<slot />
|
|
215
|
+
|
|
216
|
+
<UDashboardSidebar side="right" resizable>
|
|
217
|
+
<!-- Right sidebar -->
|
|
218
|
+
</UDashboardSidebar>
|
|
219
|
+
</UDashboardGroup>
|
|
220
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Docs Layout
|
|
2
|
+
|
|
3
|
+
Build documentation sites with sidebar navigation, table of contents, and surround links.
|
|
4
|
+
|
|
5
|
+
> Requires `@nuxt/content` module for navigation, search, and TOC.
|
|
6
|
+
|
|
7
|
+
## Component tree
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
UApp
|
|
11
|
+
├── UHeader
|
|
12
|
+
├── UMain
|
|
13
|
+
│ └── NuxtLayout (docs)
|
|
14
|
+
│ └── UPage
|
|
15
|
+
│ ├── #left → UPageAside → UContentNavigation
|
|
16
|
+
│ └── NuxtPage
|
|
17
|
+
│ ├── UPageHeader
|
|
18
|
+
│ ├── UPageBody → ContentRenderer + UContentSurround
|
|
19
|
+
│ └── #right → UContentToc
|
|
20
|
+
└── UFooter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## App shell
|
|
24
|
+
|
|
25
|
+
```vue [app.vue]
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import type { NavigationMenuItem } from '@nuxt/ui'
|
|
28
|
+
|
|
29
|
+
const route = useRoute()
|
|
30
|
+
|
|
31
|
+
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
|
32
|
+
|
|
33
|
+
provide('navigation', navigation)
|
|
34
|
+
|
|
35
|
+
const items = computed<NavigationMenuItem[]>(() => [{
|
|
36
|
+
label: 'Docs',
|
|
37
|
+
to: '/docs/getting-started',
|
|
38
|
+
active: route.path.startsWith('/docs')
|
|
39
|
+
}])
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<UApp>
|
|
44
|
+
<UHeader>
|
|
45
|
+
<template #title>
|
|
46
|
+
<Logo class="h-6 w-auto" />
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<UNavigationMenu :items="items" />
|
|
50
|
+
|
|
51
|
+
<template #right>
|
|
52
|
+
<UContentSearchButton />
|
|
53
|
+
<UColorModeButton />
|
|
54
|
+
</template>
|
|
55
|
+
</UHeader>
|
|
56
|
+
|
|
57
|
+
<UMain>
|
|
58
|
+
<NuxtLayout>
|
|
59
|
+
<NuxtPage />
|
|
60
|
+
</NuxtLayout>
|
|
61
|
+
</UMain>
|
|
62
|
+
|
|
63
|
+
<UFooter />
|
|
64
|
+
|
|
65
|
+
<UContentSearch :navigation="navigation" />
|
|
66
|
+
</UApp>
|
|
67
|
+
</template>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Layout
|
|
71
|
+
|
|
72
|
+
```vue [layouts/docs.vue]
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
import type { ContentNavigationItem } from '@nuxt/content'
|
|
75
|
+
|
|
76
|
+
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<UPage>
|
|
81
|
+
<template #left>
|
|
82
|
+
<UPageAside>
|
|
83
|
+
<UContentNavigation :navigation="navigation" />
|
|
84
|
+
</UPageAside>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<slot />
|
|
88
|
+
</UPage>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Page
|
|
93
|
+
|
|
94
|
+
```vue [pages/docs/[...slug].vue]
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
const route = useRoute()
|
|
97
|
+
|
|
98
|
+
definePageMeta({ layout: 'docs' })
|
|
99
|
+
|
|
100
|
+
const { data: page } = await useAsyncData(route.path, () => {
|
|
101
|
+
return queryCollection('docs').path(route.path).first()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
|
|
105
|
+
return queryCollectionItemSurroundings('docs', route.path)
|
|
106
|
+
})
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<UPage>
|
|
111
|
+
<UPageHeader :title="page.title" :description="page.description" />
|
|
112
|
+
|
|
113
|
+
<UPageBody>
|
|
114
|
+
<ContentRenderer :value="page" />
|
|
115
|
+
|
|
116
|
+
<USeparator />
|
|
117
|
+
|
|
118
|
+
<UContentSurround :surround="surround" />
|
|
119
|
+
</UPageBody>
|
|
120
|
+
|
|
121
|
+
<template #right>
|
|
122
|
+
<UContentToc :links="page.body.toc.links" />
|
|
123
|
+
</template>
|
|
124
|
+
</UPage>
|
|
125
|
+
</template>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> The outer `UPage` in the layout handles the left sidebar. The inner `UPage` in the page handles the right sidebar. They nest correctly.
|
|
129
|
+
|
|
130
|
+
## Key components
|
|
131
|
+
|
|
132
|
+
- `UPage` — Multi-column grid layout with `#left`, `#default`, `#right` slots
|
|
133
|
+
- `UPageAside` — Sticky sidebar wrapper (visible from `lg` breakpoint)
|
|
134
|
+
- `UPageHeader` — Page title and description
|
|
135
|
+
- `UPageBody` — Main content area
|
|
136
|
+
- `UContentNavigation` — Sidebar navigation tree
|
|
137
|
+
- `UContentToc` — Table of contents
|
|
138
|
+
- `UContentSurround` — Prev/next links
|
|
139
|
+
- `UContentSearch` / `UContentSearchButton` — Search command palette
|
|
140
|
+
- `UPageAnchors` — Simpler alternative to full TOC
|
|
141
|
+
- `UPageLinks` — Related resource links
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Editor Layout
|
|
2
|
+
|
|
3
|
+
Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop.
|
|
4
|
+
|
|
5
|
+
## Component tree
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
UApp
|
|
9
|
+
├── UHeader
|
|
10
|
+
├── UMain
|
|
11
|
+
│ └── NuxtPage
|
|
12
|
+
│ └── UContainer
|
|
13
|
+
│ └── UEditor
|
|
14
|
+
│ ├── UEditorToolbar (fixed / bubble / floating)
|
|
15
|
+
│ ├── UEditorDragHandle
|
|
16
|
+
│ ├── UEditorSuggestionMenu
|
|
17
|
+
│ ├── UEditorMentionMenu
|
|
18
|
+
│ └── UEditorEmojiMenu
|
|
19
|
+
└── UFooter
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Page
|
|
23
|
+
|
|
24
|
+
```vue [pages/editor.vue]
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const content = ref({
|
|
27
|
+
type: 'doc',
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: 'heading',
|
|
31
|
+
attrs: { level: 1 },
|
|
32
|
+
content: [{ type: 'text', text: 'Hello World' }]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'paragraph',
|
|
36
|
+
content: [{ type: 'text', text: 'Start writing...' }]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<UPage>
|
|
44
|
+
<UPageHeader title="Editor">
|
|
45
|
+
<template #actions>
|
|
46
|
+
<UButton label="Save" icon="i-lucide-save" />
|
|
47
|
+
</template>
|
|
48
|
+
</UPageHeader>
|
|
49
|
+
|
|
50
|
+
<UPageBody>
|
|
51
|
+
<UEditor v-model="content">
|
|
52
|
+
<UEditorToolbar />
|
|
53
|
+
<UEditorDragHandle />
|
|
54
|
+
<UEditorSuggestionMenu />
|
|
55
|
+
<UEditorMentionMenu
|
|
56
|
+
:items="[
|
|
57
|
+
{ label: 'Benjamin', avatar: { src: 'https://github.com/benjamincanac.png' } },
|
|
58
|
+
{ label: 'Sébastien', avatar: { src: 'https://github.com/atinux.png' } }
|
|
59
|
+
]"
|
|
60
|
+
/>
|
|
61
|
+
<UEditorEmojiMenu />
|
|
62
|
+
</UEditor>
|
|
63
|
+
</UPageBody>
|
|
64
|
+
</UPage>
|
|
65
|
+
</template>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> If you encounter prosemirror-related errors, add prosemirror packages to `vite.optimizeDeps.include` in `nuxt.config.ts`.
|
|
69
|
+
|
|
70
|
+
## Key components
|
|
71
|
+
|
|
72
|
+
- `UEditor` — Rich text editor (`v-model` accepts JSON, HTML, or markdown via `content-type` prop)
|
|
73
|
+
- `UEditorToolbar` — Toolbar with `layout`: `'fixed'` (default), `'bubble'` (on selection), `'floating'` (on empty lines)
|
|
74
|
+
- `UEditorDragHandle` — Block drag-and-drop handle
|
|
75
|
+
- `UEditorSuggestionMenu` — Slash command menu
|
|
76
|
+
- `UEditorMentionMenu` — @ mention menu
|
|
77
|
+
- `UEditorEmojiMenu` — Emoji picker
|
|
78
|
+
|
|
79
|
+
## Toolbar modes
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<!-- Fixed (default) -->
|
|
83
|
+
<UEditor v-model="content">
|
|
84
|
+
<UEditorToolbar />
|
|
85
|
+
</UEditor>
|
|
86
|
+
|
|
87
|
+
<!-- Bubble (appears on text selection) -->
|
|
88
|
+
<UEditor v-model="content">
|
|
89
|
+
<UEditorToolbar layout="bubble" />
|
|
90
|
+
</UEditor>
|
|
91
|
+
|
|
92
|
+
<!-- Floating (appears on empty lines) -->
|
|
93
|
+
<UEditor v-model="content">
|
|
94
|
+
<UEditorToolbar layout="floating" />
|
|
95
|
+
</UEditor>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Content types
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
<!-- JSON (default) -->
|
|
102
|
+
<UEditor v-model="jsonContent" />
|
|
103
|
+
|
|
104
|
+
<!-- HTML -->
|
|
105
|
+
<UEditor v-model="htmlContent" content-type="html" />
|
|
106
|
+
|
|
107
|
+
<!-- Markdown -->
|
|
108
|
+
<UEditor v-model="markdownContent" content-type="markdown" />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## With document sidebar
|
|
112
|
+
|
|
113
|
+
Combine with Dashboard components for a multi-document editor with a sidebar.
|
|
114
|
+
|
|
115
|
+
```vue [layouts/editor.vue]
|
|
116
|
+
<template>
|
|
117
|
+
<UDashboardGroup>
|
|
118
|
+
<UDashboardSidebar collapsible resizable>
|
|
119
|
+
<template #header>
|
|
120
|
+
<UButton icon="i-lucide-plus" label="New document" block />
|
|
121
|
+
</template>
|
|
122
|
+
|
|
123
|
+
<template #default>
|
|
124
|
+
<UNavigationMenu
|
|
125
|
+
:items="documents.map(doc => ({
|
|
126
|
+
label: doc.title,
|
|
127
|
+
to: `/editor/${doc.id}`,
|
|
128
|
+
icon: 'i-lucide-file-text'
|
|
129
|
+
}))"
|
|
130
|
+
orientation="vertical"
|
|
131
|
+
/>
|
|
132
|
+
</template>
|
|
133
|
+
</UDashboardSidebar>
|
|
134
|
+
|
|
135
|
+
<slot />
|
|
136
|
+
</UDashboardGroup>
|
|
137
|
+
</template>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```vue [pages/editor/[id].vue]
|
|
141
|
+
<script setup lang="ts">
|
|
142
|
+
definePageMeta({ layout: 'editor' })
|
|
143
|
+
|
|
144
|
+
const content = ref({ type: 'doc', content: [] })
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<template>
|
|
148
|
+
<UDashboardPanel>
|
|
149
|
+
<template #header>
|
|
150
|
+
<UDashboardNavbar title="Editor">
|
|
151
|
+
<template #right>
|
|
152
|
+
<UButton label="Save" icon="i-lucide-save" />
|
|
153
|
+
</template>
|
|
154
|
+
</UDashboardNavbar>
|
|
155
|
+
</template>
|
|
156
|
+
|
|
157
|
+
<UContainer class="py-8">
|
|
158
|
+
<UEditor v-model="content">
|
|
159
|
+
<UEditorToolbar />
|
|
160
|
+
<UEditorDragHandle />
|
|
161
|
+
<UEditorSuggestionMenu />
|
|
162
|
+
<UEditorEmojiMenu />
|
|
163
|
+
</UEditor>
|
|
164
|
+
</UContainer>
|
|
165
|
+
</UDashboardPanel>
|
|
166
|
+
</template>
|
|
167
|
+
```
|