@plutocms/core 0.39.2 → 0.41.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/CHANGELOG.md +19 -0
- package/app/assets/css/pluto-layer.css +8 -5
- package/app/components/Pluto/Admin/PlutoAdminNavTabs.vue +77 -0
- package/app/components/Pluto/Admin/PlutoAdminUserMenu.vue +5 -1
- package/app/components/Pluto/Content/PlutoContentBarActions.vue +5 -2
- package/app/components/Pluto/Content/PlutoContentList.vue +2 -0
- package/app/composables/pluto-admin-nav.ts +111 -8
- package/app/composables/pluto-admin-portal.ts +5 -0
- package/app/pages/admin/settings.vue +6 -33
- package/i18n/locales/en.json +0 -1
- package/i18n/locales/pt-BR.json +0 -1
- package/layers.ts +0 -1
- package/nuxt.config.ts +2 -6
- package/package.json +1 -2
- package/shared/types/registry.ts +2 -0
- package/shared/utils/content-slug.ts +1 -2
- package/shared/utils/url.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.41.0](https://github.com/plutocms/pluto/compare/v0.40.0...v0.41.0) (2026-10-31)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### ⚠ BREAKING CHANGES
|
|
8
|
+
|
|
9
|
+
* @plutocms/core does not extend @plutocms/utils anymore. Its auto-imports (formatCurrency, slugify, fileNameToKebabCase) are gone from apps that relied on core to bring them. domainFromUrl is now in core.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **admin:** section tabs for every menu item with a submenu ([#315](https://github.com/plutocms/pluto/issues/315)) ([98afbe4](https://github.com/plutocms/pluto/commit/98afbe45833caa6f834aa70703611970eadd5ccb))
|
|
14
|
+
* core no longer extends @plutocms/utils (retired); domainFromUrl is in core ([#314](https://github.com/plutocms/pluto/issues/314)) ([5fe3947](https://github.com/plutocms/pluto/commit/5fe394707c009d4686e6e7fe6f72af900dfe4035))
|
|
15
|
+
|
|
16
|
+
## [0.40.0](https://github.com/plutocms/pluto/compare/v0.39.2...v0.40.0) (2026-10-31)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **admin:** navbar menus in the bar color, palette overlay class, nav children with enabled ([#310](https://github.com/plutocms/pluto/issues/310)) ([cec92ac](https://github.com/plutocms/pluto/commit/cec92ac7061fdb8a209d832344328fae8838f826))
|
|
22
|
+
|
|
4
23
|
## [0.39.2](https://github.com/plutocms/pluto/compare/v0.39.1...v0.39.2) (2026-10-31)
|
|
5
24
|
|
|
6
25
|
|
|
@@ -69,12 +69,15 @@
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/*
|
|
72
|
-
* The overlay of the admin search
|
|
73
|
-
*
|
|
74
|
-
* the
|
|
75
|
-
*
|
|
72
|
+
* The overlay of a palette (the admin search, Cmd/Ctrl+K, and the palettes of
|
|
73
|
+
* the layers, such as the block palette) is darker than the Nuxt UI default and
|
|
74
|
+
* blurs the page under it. The overlay is the sibling just before the modal
|
|
75
|
+
* content. The content carries `pluto-admin-search` (app/layouts/admin.vue) or
|
|
76
|
+
* `pluto-palette`: a layer sets `:ui="{ content: 'pluto-palette' }"` on the
|
|
77
|
+
* `UModal` of its palette. Other modals keep the Nuxt UI overlay.
|
|
76
78
|
*/
|
|
77
|
-
:root[data-pluto-admin]
|
|
79
|
+
:root[data-pluto-admin]
|
|
80
|
+
[data-slot='overlay']:has(+ :is(.pluto-admin-search, .pluto-palette)) {
|
|
78
81
|
background-color: oklch(13% 0.03 273 / 0.6);
|
|
79
82
|
backdrop-filter: blur(6px);
|
|
80
83
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The tabs of the section of the page: the children of the menu item whose
|
|
3
|
+
// submenu holds the page (usePlutoAdminNavTabs). The "new" child of the
|
|
4
|
+
// section is a button at the end of the tabs, not a tab. Nothing shows when the
|
|
5
|
+
// page is in no submenu. `page`: under the title of a page (Settings, Users).
|
|
6
|
+
// `toolbar`: a bar under the toolbar of a list page.
|
|
7
|
+
import type { NavigationMenuItem } from '@nuxt/ui'
|
|
8
|
+
|
|
9
|
+
withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
variant?: 'page' | 'toolbar'
|
|
12
|
+
}>(),
|
|
13
|
+
{ variant: 'page' }
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const { tabs, action, label } = usePlutoAdminNavTabs()
|
|
17
|
+
|
|
18
|
+
// The active tab is set by hand (`active`), not by the router: a parent path
|
|
19
|
+
// such as `/admin/settings` must not be active on the pages below it.
|
|
20
|
+
const items = computed<NavigationMenuItem[]>(() =>
|
|
21
|
+
tabs.value.map((tab) => ({
|
|
22
|
+
label: tab.label,
|
|
23
|
+
icon: tab.icon,
|
|
24
|
+
href: tab.to,
|
|
25
|
+
active: tab.active,
|
|
26
|
+
onSelect: () => navigateTo(tab.to),
|
|
27
|
+
}))
|
|
28
|
+
)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<template v-if="items.length > 0">
|
|
33
|
+
<UDashboardToolbar
|
|
34
|
+
v-if="variant === 'toolbar'"
|
|
35
|
+
:ui="{ root: 'border-muted' }"
|
|
36
|
+
>
|
|
37
|
+
<UNavigationMenu
|
|
38
|
+
:items="items"
|
|
39
|
+
:aria-label="label"
|
|
40
|
+
orientation="horizontal"
|
|
41
|
+
variant="link"
|
|
42
|
+
class="-mx-1 min-w-0 flex-1 overflow-x-auto"
|
|
43
|
+
highlight
|
|
44
|
+
/>
|
|
45
|
+
<UButton
|
|
46
|
+
v-if="action"
|
|
47
|
+
:to="action.to"
|
|
48
|
+
:label="action.label"
|
|
49
|
+
icon="i-lucide-plus"
|
|
50
|
+
color="neutral"
|
|
51
|
+
variant="ghost"
|
|
52
|
+
size="sm"
|
|
53
|
+
class="shrink-0"
|
|
54
|
+
/>
|
|
55
|
+
</UDashboardToolbar>
|
|
56
|
+
|
|
57
|
+
<div v-else class="border-default -mt-2 flex items-center gap-2 border-b">
|
|
58
|
+
<UNavigationMenu
|
|
59
|
+
:items="items"
|
|
60
|
+
:aria-label="label"
|
|
61
|
+
orientation="horizontal"
|
|
62
|
+
variant="link"
|
|
63
|
+
class="min-w-0 flex-1 overflow-x-auto"
|
|
64
|
+
/>
|
|
65
|
+
<UButton
|
|
66
|
+
v-if="action"
|
|
67
|
+
:to="action.to"
|
|
68
|
+
:label="action.label"
|
|
69
|
+
icon="i-lucide-plus"
|
|
70
|
+
color="neutral"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="sm"
|
|
73
|
+
class="shrink-0"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
</template>
|
|
@@ -135,7 +135,11 @@ const portalTarget = usePlutoAdminPortal()
|
|
|
135
135
|
<div class="h-full">
|
|
136
136
|
<UDropdownMenu
|
|
137
137
|
:items="items"
|
|
138
|
-
:
|
|
138
|
+
:content="{ sideOffset: 2 }"
|
|
139
|
+
:ui="{
|
|
140
|
+
content: 'w-64 bg-(--pluto-admin-navbar)',
|
|
141
|
+
arrow: 'fill-(--pluto-admin-navbar)',
|
|
142
|
+
}"
|
|
139
143
|
:modal="false"
|
|
140
144
|
:portal="portalTarget ?? true"
|
|
141
145
|
:arrow="{ width: 14, height: 7 }"
|
|
@@ -214,10 +214,13 @@ const editButton = computed(() => {
|
|
|
214
214
|
<UDropdownMenu
|
|
215
215
|
:open="menuOpen"
|
|
216
216
|
:items="newMenuItems"
|
|
217
|
-
:content="{ align: 'start', onCloseAutoFocus }"
|
|
217
|
+
:content="{ align: 'start', sideOffset: 2, onCloseAutoFocus }"
|
|
218
218
|
:modal="false"
|
|
219
219
|
:portal="portalTarget ?? true"
|
|
220
|
-
:ui="{
|
|
220
|
+
:ui="{
|
|
221
|
+
content: 'min-w-48 bg-(--pluto-admin-navbar)',
|
|
222
|
+
arrow: 'fill-(--pluto-admin-navbar)',
|
|
223
|
+
}"
|
|
221
224
|
:arrow="{ width: 14, height: 7 }"
|
|
222
225
|
@update:open="onUpdateOpen"
|
|
223
226
|
>
|
|
@@ -266,6 +266,8 @@ const footer = computed(() => {
|
|
|
266
266
|
</UButton>
|
|
267
267
|
</template>
|
|
268
268
|
</PlutoViewToolbar>
|
|
269
|
+
<!-- The tabs of the menu item that holds this list (for example Pages). -->
|
|
270
|
+
<PlutoAdminNavTabs variant="toolbar" />
|
|
269
271
|
</template>
|
|
270
272
|
|
|
271
273
|
<PlutoSkeletonList
|
|
@@ -57,14 +57,23 @@ export function usePlutoAdminNav() {
|
|
|
57
57
|
|
|
58
58
|
const items = computed<PlutoNavItem[]>(() => {
|
|
59
59
|
const registered = registry.nav.list.value
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
// A child with `enabled` shows only when the test passes (for example a
|
|
61
|
+
// child for admins only). An item with its own `to` and one visible child
|
|
62
|
+
// stays a plain link, like Settings with one page.
|
|
63
|
+
const resolveLabels = (item: PlutoNavItem): PlutoNavItem => {
|
|
64
|
+
const children = item.children
|
|
65
|
+
?.filter((child) => child.enabled?.() ?? true)
|
|
66
|
+
.map((child) => ({
|
|
67
|
+
...child,
|
|
68
|
+
label: tx(child.label),
|
|
69
|
+
}))
|
|
70
|
+
return {
|
|
71
|
+
...item,
|
|
72
|
+
label: tx(item.label),
|
|
73
|
+
children:
|
|
74
|
+
item.to && children && children.length < 2 ? undefined : children,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
68
77
|
|
|
69
78
|
const isVisible = (type: PlutoContentType): boolean =>
|
|
70
79
|
!type.capabilities?.read || can(type.capabilities.read)
|
|
@@ -169,6 +178,7 @@ export function usePlutoAdminNav() {
|
|
|
169
178
|
children: settingsPages.value.map((page) => ({
|
|
170
179
|
label: page.label,
|
|
171
180
|
to: page.to,
|
|
181
|
+
icon: page.icon,
|
|
172
182
|
})),
|
|
173
183
|
}
|
|
174
184
|
: item
|
|
@@ -198,3 +208,96 @@ export function usePlutoAdminNav() {
|
|
|
198
208
|
|
|
199
209
|
return { items }
|
|
200
210
|
}
|
|
211
|
+
|
|
212
|
+
/** One tab of the section of the current page. See usePlutoAdminNavTabs. */
|
|
213
|
+
export interface PlutoAdminNavTab {
|
|
214
|
+
label: string
|
|
215
|
+
icon?: string
|
|
216
|
+
to: string
|
|
217
|
+
active: boolean
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The "new" link of a section: a button at the end of its tabs. */
|
|
221
|
+
export interface PlutoAdminNavAction {
|
|
222
|
+
label: string
|
|
223
|
+
to: string
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Trailing slashes off, so `/admin/users/` matches `/admin/users`.
|
|
227
|
+
function trimPath(path: string): string {
|
|
228
|
+
return path.replace(/\/+$/, '') || '/'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The tabs of the section of the current page: the children of the menu item
|
|
233
|
+
* whose submenu holds the page. Settings, the Users item of a backend layer and
|
|
234
|
+
* a group of content types (Pages) all get them.
|
|
235
|
+
*
|
|
236
|
+
* - A child matches the path exactly, or as a prefix (`/admin/users` matches
|
|
237
|
+
* `/admin/users/5`). The longest match wins, so General (`/admin/settings`)
|
|
238
|
+
* is not active on `/admin/settings/email`.
|
|
239
|
+
* - A "new" child (its `to` is the `newTo` of the item, or ends with `/new`)
|
|
240
|
+
* is not a tab: it is `action`, a button at the end of the tabs.
|
|
241
|
+
* - The tabs show only with two or more of them. A section of one list and its
|
|
242
|
+
* "new" page (a content type: All posts, New post) shows none: the list page
|
|
243
|
+
* has its own New button.
|
|
244
|
+
*
|
|
245
|
+
* `label` is the label of the section (for `aria-label`). A page shows the
|
|
246
|
+
* tabs with `PlutoAdminNavTabs`.
|
|
247
|
+
*/
|
|
248
|
+
export function usePlutoAdminNavTabs() {
|
|
249
|
+
const { items } = usePlutoAdminNav()
|
|
250
|
+
const route = useRoute()
|
|
251
|
+
|
|
252
|
+
const isNewChild = (item: PlutoNavItem, child: PlutoNavChild): boolean =>
|
|
253
|
+
(item.newTo !== undefined && trimPath(child.to) === trimPath(item.newTo)) ||
|
|
254
|
+
trimPath(child.to).endsWith('/new')
|
|
255
|
+
|
|
256
|
+
const section = computed(() => {
|
|
257
|
+
const path = trimPath(route.path)
|
|
258
|
+
let best: { item: PlutoNavItem; to: string } | undefined
|
|
259
|
+
for (const item of items.value) {
|
|
260
|
+
const pages = (item.children ?? []).filter(
|
|
261
|
+
(child) => !isNewChild(item, child)
|
|
262
|
+
)
|
|
263
|
+
if (pages.length < 2) {
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
for (const child of pages) {
|
|
267
|
+
const to = trimPath(child.to)
|
|
268
|
+
const matches = path === to || path.startsWith(`${to}/`)
|
|
269
|
+
if (matches && (!best || to.length > best.to.length)) {
|
|
270
|
+
best = { item, to }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return best
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const tabs = computed<PlutoAdminNavTab[]>(() => {
|
|
278
|
+
const current = section.value
|
|
279
|
+
if (!current) {
|
|
280
|
+
return []
|
|
281
|
+
}
|
|
282
|
+
return (current.item.children ?? [])
|
|
283
|
+
.filter((child) => !isNewChild(current.item, child))
|
|
284
|
+
.map((child) => ({
|
|
285
|
+
label: child.label,
|
|
286
|
+
icon: child.icon,
|
|
287
|
+
to: child.to,
|
|
288
|
+
active: trimPath(child.to) === current.to,
|
|
289
|
+
}))
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const action = computed<PlutoAdminNavAction | undefined>(() => {
|
|
293
|
+
const item = section.value?.item
|
|
294
|
+
const child = item?.children?.find((entry) => isNewChild(item, entry))
|
|
295
|
+
return child ? { label: child.label, to: child.to } : undefined
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
tabs,
|
|
300
|
+
action,
|
|
301
|
+
label: computed(() => section.value?.item.label ?? ''),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* The composable makes the element itself. A target that Vue renders can
|
|
9
9
|
* appear after the portal looks for it. Pass `portalTarget ?? true` to the
|
|
10
10
|
* `portal` prop of `UDropdownMenu`.
|
|
11
|
+
*
|
|
12
|
+
* The menus look like the bar: the target also carries `data-pluto-admin-dark`
|
|
13
|
+
* (the dark tokens in both color modes), and a menu sets the bar color
|
|
14
|
+
* (`bg-(--pluto-admin-navbar)`, also on its arrow) and `sideOffset: 2`.
|
|
11
15
|
*/
|
|
12
16
|
export function usePlutoAdminPortal() {
|
|
13
17
|
const portalTarget = shallowRef<HTMLElement>()
|
|
14
18
|
onMounted(() => {
|
|
15
19
|
const el = document.createElement('div')
|
|
16
20
|
el.setAttribute('data-pluto-admin-scope', '')
|
|
21
|
+
el.setAttribute('data-pluto-admin-dark', '')
|
|
17
22
|
document.body.append(el)
|
|
18
23
|
portalTarget.value = el
|
|
19
24
|
})
|
|
@@ -1,36 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The parent of every Settings page: the title, the tabs, and the child route.
|
|
3
3
|
// A layer adds a tab with a file at `pages/admin/settings/<x>.vue` and a
|
|
4
|
-
// `settingsPages` entry.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// `settingsPages` entry. The tabs are the children of the Settings item of the
|
|
5
|
+
// menu (PlutoAdminNavTabs), like every item with a submenu. See the
|
|
6
|
+
// extension-registry skill.
|
|
7
7
|
const { t } = usePlutoI18n()
|
|
8
|
-
const route = useRoute()
|
|
9
8
|
|
|
10
9
|
useHead({
|
|
11
10
|
title: () => t('pluto.pages.settings'),
|
|
12
11
|
})
|
|
13
12
|
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
// Active state by hand: General (`/admin/settings`) matches exactly, so it is
|
|
17
|
-
// not active on the pages below it. Any other tab matches by prefix.
|
|
18
|
-
function isActive(to: string): boolean {
|
|
19
|
-
const path = route.path.replace(/\/+$/, '') || '/'
|
|
20
|
-
return to === '/admin/settings'
|
|
21
|
-
? path === to
|
|
22
|
-
: path === to || path.startsWith(`${to}/`)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const tabs = computed<NavigationMenuItem[]>(() =>
|
|
26
|
-
items.value.map((page) => ({
|
|
27
|
-
label: page.label,
|
|
28
|
-
icon: page.icon,
|
|
29
|
-
href: page.to,
|
|
30
|
-
active: isActive(page.to),
|
|
31
|
-
onSelect: () => navigateTo(page.to),
|
|
32
|
-
}))
|
|
33
|
-
)
|
|
13
|
+
const { current, allowed } = usePlutoSettingsPages()
|
|
34
14
|
</script>
|
|
35
15
|
|
|
36
16
|
<template>
|
|
@@ -40,15 +20,8 @@ const tabs = computed<NavigationMenuItem[]>(() =>
|
|
|
40
20
|
{{ t('pluto.pages.settings') }}
|
|
41
21
|
</h1>
|
|
42
22
|
|
|
43
|
-
<!-- One page (General only)
|
|
44
|
-
<
|
|
45
|
-
v-if="tabs.length >= 2"
|
|
46
|
-
:items="tabs"
|
|
47
|
-
:aria-label="t('pluto.settings.tabs.label')"
|
|
48
|
-
orientation="horizontal"
|
|
49
|
-
variant="link"
|
|
50
|
-
class="border-default -mt-2 overflow-x-auto border-b"
|
|
51
|
-
/>
|
|
23
|
+
<!-- One page (General only) has no submenu, so no tabs. -->
|
|
24
|
+
<PlutoAdminNavTabs />
|
|
52
25
|
|
|
53
26
|
<UAlert
|
|
54
27
|
v-if="!allowed"
|
package/i18n/locales/en.json
CHANGED
package/i18n/locales/pt-BR.json
CHANGED
package/layers.ts
CHANGED
|
@@ -28,7 +28,6 @@ export type PlutoLayerEntry = [source: string, options: PlutoLayerEntryOptions]
|
|
|
28
28
|
* without the scope. A package of a monorepo is in a folder of its repo.
|
|
29
29
|
*/
|
|
30
30
|
export const PLUTO_LAYER_PATHS: Readonly<Record<string, string>> = {
|
|
31
|
-
utils: 'utils',
|
|
32
31
|
core: 'pluto/packages/core',
|
|
33
32
|
supabase: 'supabase',
|
|
34
33
|
'supabase-shop': 'supabase-shop',
|
package/nuxt.config.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
const websiteName = process.env.WEBSITE_NAME || 'Untitled'
|
|
2
2
|
|
|
3
|
-
// Point PLUTO_UTILS_PATH at a local checkout (e.g. `../../../utils`) to test
|
|
4
|
-
// unpublished changes; unset, it resolves to the published npm package.
|
|
5
|
-
const utilsLayer = process.env.PLUTO_UTILS_PATH || '@plutocms/utils'
|
|
6
|
-
|
|
7
3
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
4
|
+
// Core extends no other layer. The helpers of the retired `@plutocms/utils` that
|
|
5
|
+
// core uses are in shared/utils (`domainFromUrl`).
|
|
8
6
|
export default defineNuxtConfig({
|
|
9
|
-
extends: [utilsLayer],
|
|
10
|
-
|
|
11
7
|
// No @nuxt/eslint: a module of a layer runs in every app that extends it. The
|
|
12
8
|
// repo lints with @nuxt/eslint-config (eslint.config.mjs at the root).
|
|
13
9
|
modules: [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.41.0",
|
|
5
5
|
"description": "The base layer every Pluto app extends: the admin, the content model, and the GitHub CMS",
|
|
6
6
|
"author": "Victor Ribeiro",
|
|
7
7
|
"license": "MIT",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"@lezer/highlight": "^1.2.5",
|
|
44
44
|
"@nuxt/image": "^2.1.0",
|
|
45
45
|
"@nuxt/ui": "^4.12.0",
|
|
46
|
-
"@plutocms/utils": ">=0.3.0 <1.0.0",
|
|
47
46
|
"@tanstack/charts": "^1.1.0",
|
|
48
47
|
"@vueuse/nuxt": "^15.0.0",
|
|
49
48
|
"image-meta": "^0.3.0",
|
package/shared/types/registry.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface PlutoNavChild {
|
|
|
59
59
|
newTo?: string
|
|
60
60
|
/** The text of that "+" (`New <type>`). */
|
|
61
61
|
newLabel?: string
|
|
62
|
+
/** Reactive visibility test, like `PlutoEntry.enabled`. A child without it shows. */
|
|
63
|
+
enabled?: () => boolean
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface PlutoNavItem extends PlutoEntry {
|
|
@@ -6,8 +6,7 @@ export const SLUG_MAX_LENGTH = 100
|
|
|
6
6
|
* "pao-de-mel". The result is lowercase ASCII, with single dashes. It can
|
|
7
7
|
* be an empty string.
|
|
8
8
|
*
|
|
9
|
-
* The form and the GitHub CMS both use this function.
|
|
10
|
-
* `slugify`: `@plutocms/utils` already auto-imports that name.
|
|
9
|
+
* The form and the GitHub CMS both use this function.
|
|
11
10
|
*/
|
|
12
11
|
export function slugifyTitle(value: string): string {
|
|
13
12
|
return value
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host of a URL, without the scheme and `www.`: `https://www.example.com/a`
|
|
3
|
+
* gives `example.com`. No URL gives `undefined`; a value that does not parse
|
|
4
|
+
* gives an empty string. The admin bar shows it next to "View site".
|
|
5
|
+
*/
|
|
6
|
+
export function domainFromUrl(url: string | null | undefined) {
|
|
7
|
+
if (!url) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^/]+)/i)
|
|
12
|
+
return match ? match[1] : ''
|
|
13
|
+
}
|