@plutocms/core 0.45.0 → 0.47.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 +27 -0
- package/FEATURES.md +10 -3
- package/app/assets/css/pluto-base.css +228 -0
- package/app/assets/css/pluto-layer.css +6 -10
- package/app/components/Pluto/Admin/PlutoAdminNavbarProvider.vue +2 -1
- package/app/components/Pluto/Admin/PlutoAdminTheme.vue +4 -0
- package/app/components/Pluto/PlutoHtml.vue +6 -21
- package/app/components/Pluto/PlutoRoot.vue +56 -4
- package/app/composables/pluto-admin-theme.ts +21 -0
- package/modules/pluto-css/css-entries.ts +48 -58
- package/modules/pluto-css/index.ts +110 -39
- package/modules/pluto-css/scope.ts +117 -0
- package/nuxt.config.ts +4 -3
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.47.0](https://github.com/plutocms/pluto/compare/v0.46.0...v0.47.0) (2026-11-02)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### ⚠ BREAKING CHANGES
|
|
8
|
+
|
|
9
|
+
* PlutoHtml has no Tailwind prose classes. A site imports `@plutocms/css/base` for the prose look. A site that adds its own page offset for the admin bar removes it, or the page moves down twice.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* the public components use plain CSS, with an optional @plutocms/css/base ([#353](https://github.com/plutocms/pluto/issues/353)) ([d0fa541](https://github.com/plutocms/pluto/commit/d0fa541b34a672370ebf2d95372a967a1aebde10))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* the admin has its styles in the first frame of a client navigation ([#354](https://github.com/plutocms/pluto/issues/354)) ([f7a4226](https://github.com/plutocms/pluto/commit/f7a4226966e73bf2a91ae599303c35a8daac61a2))
|
|
19
|
+
|
|
20
|
+
## [0.46.0](https://github.com/plutocms/pluto/compare/v0.45.0...v0.46.0) (2026-11-02)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### ⚠ BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
* an app with no Tailwind entry no longer gets one from Pluto. A site whose pages use Tailwind or Nuxt UI adds its own entry.
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* the admin has its own Tailwind build, and a site needs no Tailwind ([#343](https://github.com/plutocms/pluto/issues/343)) ([6d6ae4a](https://github.com/plutocms/pluto/commit/6d6ae4a59f14289abf2da421d028dba920cacd76))
|
|
30
|
+
|
|
4
31
|
## [0.45.0](https://github.com/plutocms/pluto/compare/v0.44.1...v0.45.0) (2026-11-01)
|
|
5
32
|
|
|
6
33
|
|
package/FEATURES.md
CHANGED
|
@@ -66,9 +66,16 @@ policies shared by every project here.
|
|
|
66
66
|
@.claude/skills/i18n/SKILL.md
|
|
67
67
|
- Settings pages: `/admin/settings` is a section with tabs. A layer adds a tab with a `settingsPages` entry and a page file at `pages/admin/settings/<x>.vue`. The tabs show with 2 or more pages, the sidebar and the command palette list them, and a tab can need a capability (`'*'` is admin only). The capability picker groups by layer. @.claude/skills/extension-registry/SKILL.md
|
|
68
68
|
- Mail: a mail driver contract (`sendPlutoMail`, `registerMailDriver`) with Resend, dev (console and Mailpit) and none drivers, plain-TypeScript email templates (test, invite) translated by a server translator, and a guided `Settings > Email` page (admin only) that checks the key, adds and verifies a domain, and tests the setup. Secrets stay in the environment. @.claude/skills/mail/SKILL.md
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
- Admin and site CSS: the admin has its own Tailwind build, scoped to the admin and loaded only
|
|
70
|
+
with the admin UI, so a site needs no Tailwind and a site's theme does not reach the admin. A
|
|
71
|
+
Tailwind site owns its one build. The pluto-css module generates `#build/pluto.css` from each
|
|
72
|
+
layer's `pluto-layer.css` (imported as `@plutocms/css` by the admin build and by a Tailwind
|
|
73
|
+
site), and warns about a Tailwind build in a layer or more than one.
|
|
74
|
+
@.claude/skills/single-tailwind-build/SKILL.md
|
|
75
|
+
- Public components: the components for a public site (`PlutoHtml`, `PlutoImage`, `PlutoLogo`,
|
|
76
|
+
`PlutoRoot`, and those of the layers) use only `pluto-*` classes and work with no Tailwind. Their
|
|
77
|
+
look is plain CSS in each layer's `pluto-base.css`, which a site imports with the opt-in
|
|
78
|
+
`@import '@plutocms/css/base'`. @.claude/skills/public-components/SKILL.md
|
|
72
79
|
- Git CMS: a standalone CMS with Markdown posts in a Git repo. Out of the box it publishes to the
|
|
73
80
|
local Git repo (dev only: each save is a local commit, `commit.local`); a GitHub App publishes
|
|
74
81
|
from a deployed site. The module is `pluto-github` (config
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The look of the public components of core. Opt-in: a site imports it with
|
|
3
|
+
* `@import '@plutocms/css/base'`, with or without Tailwind. Plain CSS only:
|
|
4
|
+
* no Tailwind directive. See the public-components skill.
|
|
5
|
+
*
|
|
6
|
+
* Every selector is in `:where()` (zero specificity) and in the `components`
|
|
7
|
+
* layer. On a Tailwind site, the file joins Tailwind's own layer, so a utility
|
|
8
|
+
* wins. On a site with no Tailwind, any unlayered rule of the site wins.
|
|
9
|
+
*
|
|
10
|
+
* Colors read the Nuxt UI variables, with plain fallbacks: a Nuxt UI site
|
|
11
|
+
* gets its own theme. A site can also set the `--pluto-prose-*` variables.
|
|
12
|
+
*/
|
|
13
|
+
@layer theme, base, components, utilities;
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
/* PlutoHtml, PlutoMarkdown, and the PlutoProse of the blog layer. */
|
|
17
|
+
:where(.pluto-prose) {
|
|
18
|
+
--pluto-prose-heading: var(--ui-text-highlighted, currentColor);
|
|
19
|
+
--pluto-prose-muted: var(
|
|
20
|
+
--ui-text-muted,
|
|
21
|
+
color-mix(in oklab, currentColor 65%, transparent)
|
|
22
|
+
);
|
|
23
|
+
--pluto-prose-link: var(--ui-primary, LinkText);
|
|
24
|
+
--pluto-prose-border: var(
|
|
25
|
+
--ui-border,
|
|
26
|
+
color-mix(in oklab, currentColor 15%, transparent)
|
|
27
|
+
);
|
|
28
|
+
--pluto-prose-border-accented: var(
|
|
29
|
+
--ui-border-accented,
|
|
30
|
+
color-mix(in oklab, currentColor 30%, transparent)
|
|
31
|
+
);
|
|
32
|
+
--pluto-prose-code-bg: var(
|
|
33
|
+
--ui-bg-muted,
|
|
34
|
+
color-mix(in oklab, currentColor 6%, transparent)
|
|
35
|
+
);
|
|
36
|
+
--pluto-prose-th-bg: var(
|
|
37
|
+
--ui-bg-elevated,
|
|
38
|
+
color-mix(in oklab, currentColor 4%, transparent)
|
|
39
|
+
);
|
|
40
|
+
--pluto-prose-radius: calc(var(--ui-radius, 0.25rem) * 1.5);
|
|
41
|
+
--pluto-prose-mono: var(
|
|
42
|
+
--font-mono,
|
|
43
|
+
ui-monospace,
|
|
44
|
+
SFMono-Regular,
|
|
45
|
+
Menlo,
|
|
46
|
+
Consolas,
|
|
47
|
+
monospace
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
width: 100%;
|
|
51
|
+
overflow-wrap: break-word;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:where(.pluto-prose) ::selection {
|
|
55
|
+
background-color: color-mix(
|
|
56
|
+
in oklab,
|
|
57
|
+
var(--pluto-prose-link) 20%,
|
|
58
|
+
transparent
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:where(.pluto-prose) > :where(*) {
|
|
63
|
+
margin-block: 1.25rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:where(.pluto-prose) > :where(:first-child) {
|
|
67
|
+
margin-block-start: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:where(.pluto-prose) > :where(:last-child) {
|
|
71
|
+
margin-block-end: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:where(.pluto-prose) :where(p) {
|
|
75
|
+
line-height: 1.75rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:where(.pluto-prose) :where(a) {
|
|
79
|
+
color: var(--pluto-prose-link);
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
text-decoration: none;
|
|
82
|
+
border-block-end: 1px solid transparent;
|
|
83
|
+
transition: border-color 0.15s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:where(.pluto-prose) :where(a:hover) {
|
|
87
|
+
border-block-end-color: var(--pluto-prose-link);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:where(.pluto-prose) :where(a:focus-visible) {
|
|
91
|
+
border-block-end-color: transparent;
|
|
92
|
+
border-radius: 0.125rem;
|
|
93
|
+
outline: 3px solid
|
|
94
|
+
color-mix(in oklab, var(--pluto-prose-link) 25%, transparent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:where(.pluto-prose) :where(h1, h2, h3, h4, h5, h6) {
|
|
98
|
+
color: var(--pluto-prose-heading);
|
|
99
|
+
font-weight: 700;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
:where(.pluto-prose) :where(h1) {
|
|
103
|
+
font-size: 1.875rem;
|
|
104
|
+
line-height: 2.25rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
:where(.pluto-prose) :where(h2) {
|
|
108
|
+
font-size: 1.5rem;
|
|
109
|
+
line-height: 2rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
:where(.pluto-prose) :where(h3) {
|
|
113
|
+
font-size: 1.25rem;
|
|
114
|
+
line-height: 1.75rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:where(.pluto-prose) :where(h4) {
|
|
118
|
+
font-size: 1.125rem;
|
|
119
|
+
line-height: 1.75rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
:where(.pluto-prose) :where(h5, h6) {
|
|
123
|
+
font-size: 1rem;
|
|
124
|
+
line-height: 1.5rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
:where(.pluto-prose) :where(blockquote) {
|
|
128
|
+
margin-inline: 0;
|
|
129
|
+
padding-inline-start: 1rem;
|
|
130
|
+
border-inline-start: 4px solid var(--pluto-prose-border-accented);
|
|
131
|
+
font-style: italic;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
:where(.pluto-prose) :where(hr) {
|
|
135
|
+
border: 0;
|
|
136
|
+
border-block-start: 1px solid var(--pluto-prose-border);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
:where(.pluto-prose) :where(code) {
|
|
140
|
+
display: inline-block;
|
|
141
|
+
padding: 0.125rem 0.375rem;
|
|
142
|
+
border: 1px solid var(--pluto-prose-border);
|
|
143
|
+
border-radius: var(--pluto-prose-radius);
|
|
144
|
+
background-color: var(--pluto-prose-code-bg);
|
|
145
|
+
color: var(--pluto-prose-heading);
|
|
146
|
+
font-family: var(--pluto-prose-mono);
|
|
147
|
+
font-size: 0.875rem;
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:where(.pluto-prose) :where(pre) {
|
|
152
|
+
overflow-x: auto;
|
|
153
|
+
padding: 0.75rem 1rem;
|
|
154
|
+
border: 1px solid var(--pluto-prose-border);
|
|
155
|
+
border-radius: var(--pluto-prose-radius);
|
|
156
|
+
background-color: var(--pluto-prose-code-bg);
|
|
157
|
+
font-family: var(--pluto-prose-mono);
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
line-height: 1.5rem;
|
|
160
|
+
white-space: pre-wrap;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
:where(.pluto-prose) :where(pre code) {
|
|
164
|
+
display: inline;
|
|
165
|
+
padding: 0;
|
|
166
|
+
border: 0;
|
|
167
|
+
border-radius: 0;
|
|
168
|
+
background-color: transparent;
|
|
169
|
+
color: inherit;
|
|
170
|
+
font-size: inherit;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
:where(.pluto-prose) :where(ul, ol) {
|
|
174
|
+
padding-inline-start: 1.5rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
:where(.pluto-prose) :where(ul) {
|
|
178
|
+
list-style-type: disc;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
:where(.pluto-prose) :where(ol) {
|
|
182
|
+
list-style-type: decimal;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
:where(.pluto-prose) :where(ul > li)::marker {
|
|
186
|
+
color: var(--pluto-prose-border-accented);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
:where(.pluto-prose) :where(ol > li)::marker {
|
|
190
|
+
color: var(--pluto-prose-muted);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
:where(.pluto-prose) :where(li) {
|
|
194
|
+
margin-block: 0.375rem;
|
|
195
|
+
padding-inline-start: 0.375rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:where(.pluto-prose) :where(img) {
|
|
199
|
+
display: block;
|
|
200
|
+
max-width: 100%;
|
|
201
|
+
height: auto;
|
|
202
|
+
border-radius: var(--pluto-prose-radius);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:where(.pluto-prose) :where(table) {
|
|
206
|
+
display: block;
|
|
207
|
+
overflow-x: auto;
|
|
208
|
+
width: 100%;
|
|
209
|
+
border-collapse: collapse;
|
|
210
|
+
font-size: 0.875rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
:where(.pluto-prose) :where(th, td) {
|
|
214
|
+
padding: 0.5rem 0.75rem;
|
|
215
|
+
border: 1px solid var(--pluto-prose-border);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
:where(.pluto-prose) :where(th) {
|
|
219
|
+
background-color: var(--pluto-prose-th-bg);
|
|
220
|
+
color: var(--pluto-prose-heading);
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
text-align: start;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
:where(.pluto-prose) :where(input[type='checkbox']) {
|
|
226
|
+
margin-inline-end: 0.5rem;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The pluto layer's CSS. It is not a Tailwind entry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* `@plutocms/css` (core generates it) imports this file (and the same file of every other
|
|
5
|
+
* Pluto layer). Two builds import `@plutocms/css`: the admin build of core
|
|
6
|
+
* (scoped to the admin), and the app's own CSS entry of a Tailwind site:
|
|
5
7
|
*
|
|
6
8
|
* @import 'tailwindcss';
|
|
7
9
|
* @import '@nuxt/ui';
|
|
8
10
|
* @import '@plutocms/css';
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* Pluto layer). Rules for this file:
|
|
12
|
+
* Rules for this file:
|
|
12
13
|
* - Do not import tailwindcss or @nuxt/ui here.
|
|
13
14
|
* - Use only @theme, @custom-variant, @utility, unlayered rules, and
|
|
14
15
|
* imports of plain CSS files next to this one.
|
|
@@ -26,12 +27,8 @@
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/*
|
|
29
|
-
* `--pluto-admin-bar-height`
|
|
30
|
-
*
|
|
31
|
-
* that shows the admin navbar to a logged-in admin offsets its own header by
|
|
32
|
-
* the same value. Core sets `data-pluto-admin-bar` on <html> while the bar
|
|
33
|
-
* shows, so a site can offset its header only then:
|
|
34
|
-
* `:root[data-pluto-admin-bar] .site-header { top: var(--pluto-admin-bar-height) }`.
|
|
30
|
+
* `--pluto-admin-bar-height` (the admin navbar height) is in PlutoRoot, in
|
|
31
|
+
* plain CSS, so a site with no Tailwind has it too.
|
|
35
32
|
*
|
|
36
33
|
* `--ui-header-height` stays 2.5rem for existing apps. Set it outside a layer.
|
|
37
34
|
* This overrides Nuxt UI's own `@layer theme` value for the same variable. A
|
|
@@ -39,7 +36,6 @@
|
|
|
39
36
|
* `--ui-header-height`, and the admin keeps `--pluto-admin-bar-height`.
|
|
40
37
|
*/
|
|
41
38
|
:root {
|
|
42
|
-
--pluto-admin-bar-height: 2.5rem;
|
|
43
39
|
--ui-header-height: 2.5rem;
|
|
44
40
|
}
|
|
45
41
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Decides if the core admin navbar shows. The rule lives in `usePlutoAdminBar`.
|
|
3
|
+
// The navbar is lazy, so its code and the admin CSS load only when it shows.
|
|
3
4
|
const { visible } = usePlutoAdminBar()
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
7
|
<template>
|
|
7
|
-
<
|
|
8
|
+
<LazyPlutoAdminNavbar v-if="visible" />
|
|
8
9
|
</template>
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
PLUTO_ADMIN_UI,
|
|
9
9
|
PLUTO_ADMIN_UI_PROPS,
|
|
10
10
|
} from '../../../composables/pluto-admin-theme'
|
|
11
|
+
// The admin's own Tailwind build, scoped to the admin. Only the async chunks of
|
|
12
|
+
// the admin UI import this component, so a visitor who sees no admin UI does
|
|
13
|
+
// not load the build. See the pluto-css module.
|
|
14
|
+
import '#build/pluto-admin.css'
|
|
11
15
|
</script>
|
|
12
16
|
|
|
13
17
|
<template>
|
|
@@ -1,36 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { tv } from '@nuxt/ui/utils/tv'
|
|
3
|
-
import theme from '#build/ui/editor'
|
|
4
|
-
|
|
5
2
|
// Shows the `html` of a public item (the public API of Pluto), with readable prose styles. It
|
|
6
3
|
// works with every backend: the Git CMS and `@plutocms/supabase-blog` both send the body as
|
|
7
4
|
// sanitized HTML, so the string is safe to set as `innerHTML`. This component never parses or
|
|
8
5
|
// sanitizes. Do not pass any other HTML to it.
|
|
9
|
-
|
|
6
|
+
//
|
|
7
|
+
// The prose look is `.pluto-prose` in app/assets/css/pluto-base.css: plain CSS that a site
|
|
8
|
+
// imports with `@plutocms/css/base`. No Tailwind class here, so the component works on a site
|
|
9
|
+
// with no Tailwind. See the public-components skill.
|
|
10
|
+
withDefaults(
|
|
10
11
|
defineProps<{
|
|
11
12
|
html: string
|
|
12
13
|
tag?: string
|
|
13
|
-
class?: any
|
|
14
14
|
}>(),
|
|
15
15
|
{
|
|
16
16
|
tag: 'article',
|
|
17
|
-
class: undefined,
|
|
18
17
|
}
|
|
19
18
|
)
|
|
20
|
-
|
|
21
|
-
// The base of the Nuxt UI editor theme gives readable prose with semantic
|
|
22
|
-
// color tokens. The editor adds side padding, so `sm:px-0` removes it.
|
|
23
|
-
// The table classes are here because the editor theme has none.
|
|
24
|
-
const ui = computed(() => tv({ extend: theme })())
|
|
25
|
-
|
|
26
|
-
const classes = computed(() => [
|
|
27
|
-
ui.value.base({ class: 'sm:px-0' }),
|
|
28
|
-
'[&_table]:block [&_table]:w-full [&_table]:overflow-x-auto [&_table]:text-sm',
|
|
29
|
-
'[&_:is(th,td)]:border [&_:is(th,td)]:border-default [&_:is(th,td)]:px-3 [&_:is(th,td)]:py-2',
|
|
30
|
-
'[&_th]:bg-elevated [&_th]:text-highlighted [&_th]:text-start [&_th]:font-semibold',
|
|
31
|
-
'[&_input[type=checkbox]]:me-2',
|
|
32
|
-
props.class,
|
|
33
|
-
])
|
|
34
19
|
</script>
|
|
35
20
|
|
|
36
21
|
<template>
|
|
@@ -38,5 +23,5 @@ const classes = computed(() => [
|
|
|
38
23
|
on a dynamic `<component :is>` drops the value in the SSR output. The
|
|
39
24
|
hyphenated spelling renders a literal `inner-h-t-m-l` attribute. -->
|
|
40
25
|
<!-- eslint-disable-next-line vue/attribute-hyphenation -- must stay camelCase, see comment above -->
|
|
41
|
-
<component :is="tag" v-if="html" :
|
|
26
|
+
<component :is="tag" v-if="html" :innerHTML="html" class="pluto-prose" />
|
|
42
27
|
</template>
|
|
@@ -114,8 +114,8 @@ useHead({
|
|
|
114
114
|
</script>
|
|
115
115
|
|
|
116
116
|
<template>
|
|
117
|
-
<div class="
|
|
118
|
-
<UApp :locale="uiLocale"
|
|
117
|
+
<div class="pluto-root">
|
|
118
|
+
<UApp :locale="uiLocale">
|
|
119
119
|
<NuxtRouteAnnouncer />
|
|
120
120
|
<NuxtLoadingIndicator />
|
|
121
121
|
|
|
@@ -125,9 +125,9 @@ useHead({
|
|
|
125
125
|
Public pages have no admin sidebar. The admin navbar menu button
|
|
126
126
|
opens this slideover there instead.
|
|
127
127
|
-->
|
|
128
|
-
<
|
|
128
|
+
<LazyPlutoNavbarAdminMenu v-if="!isAdmin && adminBar.visible.value" />
|
|
129
129
|
|
|
130
|
-
<div class="
|
|
130
|
+
<div class="pluto-root-page">
|
|
131
131
|
<NuxtLayout>
|
|
132
132
|
<slot />
|
|
133
133
|
</NuxtLayout>
|
|
@@ -149,6 +149,58 @@ useHead({
|
|
|
149
149
|
flex-direction: column;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/*
|
|
153
|
+
* `--pluto-admin-bar-height` is the height of the admin navbar: 2.5rem (40px)
|
|
154
|
+
* at every width. The admin navbar and the admin layout read it. A public site
|
|
155
|
+
* that shows the bar to a logged-in admin offsets its own header by the same
|
|
156
|
+
* value. Core sets `data-pluto-admin-bar` on <html> while the bar shows, so a
|
|
157
|
+
* site can offset its header only then:
|
|
158
|
+
* `:root[data-pluto-admin-bar] .site-header { top: var(--pluto-admin-bar-height) }`.
|
|
159
|
+
* It is here, in plain CSS, so a site with no Tailwind has it too.
|
|
160
|
+
*/
|
|
161
|
+
:root {
|
|
162
|
+
--pluto-admin-bar-height: 2.5rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* The bar is fixed at the top, so a public page moves down by its height while
|
|
167
|
+
* it shows: the top of the page is never under the bar. A sticky or fixed
|
|
168
|
+
* header of the site still needs `top: var(--pluto-admin-bar-height)`. The
|
|
169
|
+
* admin pages (`data-pluto-admin`) have their own layout. Zero specificity
|
|
170
|
+
* (`:where`), so a rule of the site wins.
|
|
171
|
+
*/
|
|
172
|
+
:where(:root[data-pluto-admin-bar]:not([data-pluto-admin])) body {
|
|
173
|
+
padding-top: var(--pluto-admin-bar-height);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
:where(:root[data-pluto-admin-bar]:not([data-pluto-admin])) {
|
|
177
|
+
scroll-padding-top: var(--pluto-admin-bar-height);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
:where(:root[data-pluto-admin-bar]:not([data-pluto-admin])) #__nuxt {
|
|
181
|
+
min-height: calc(100dvh - var(--pluto-admin-bar-height));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* The wrappers grow to fill #__nuxt. Plain CSS, not Tailwind classes: a site
|
|
186
|
+
* may have no Tailwind (the admin has a build of its own).
|
|
187
|
+
*/
|
|
188
|
+
.pluto-root {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
flex-grow: 1;
|
|
192
|
+
width: 100%;
|
|
193
|
+
min-width: 0;
|
|
194
|
+
min-height: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.pluto-root-page {
|
|
198
|
+
flex-grow: 1;
|
|
199
|
+
width: 100%;
|
|
200
|
+
min-width: 0;
|
|
201
|
+
min-height: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
152
204
|
:root[data-pluto-admin],
|
|
153
205
|
:root[data-pluto-admin] body,
|
|
154
206
|
:root[data-pluto-admin] #__nuxt {
|
|
@@ -8,9 +8,30 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Call it from every admin layout. `useHead` removes the attribute when the
|
|
10
10
|
* layout unmounts, so the public site never gets it.
|
|
11
|
+
*
|
|
12
|
+
* `useHead` puts the attribute in the server HTML, but in the browser it
|
|
13
|
+
* writes <html> after the render, a frame late. The admin build is scoped to
|
|
14
|
+
* the attribute, so a client navigation from the public site showed one frame
|
|
15
|
+
* of the admin with no styles. `onMounted` runs in the same task as the DOM
|
|
16
|
+
* change, before the paint, so the attribute is set there too. A count keeps
|
|
17
|
+
* it while another admin layout is mounted.
|
|
11
18
|
*/
|
|
19
|
+
let mountedAdminLayouts = 0
|
|
20
|
+
|
|
12
21
|
export function usePlutoAdminTheme() {
|
|
13
22
|
useHead({ htmlAttrs: { 'data-pluto-admin': '' } })
|
|
23
|
+
if (import.meta.client) {
|
|
24
|
+
onMounted(() => {
|
|
25
|
+
mountedAdminLayouts++
|
|
26
|
+
document.documentElement.setAttribute('data-pluto-admin', '')
|
|
27
|
+
})
|
|
28
|
+
onUnmounted(() => {
|
|
29
|
+
mountedAdminLayouts--
|
|
30
|
+
if (mountedAdminLayouts === 0) {
|
|
31
|
+
document.documentElement.removeAttribute('data-pluto-admin')
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
14
35
|
}
|
|
15
36
|
|
|
16
37
|
/**
|
|
@@ -11,24 +11,30 @@ export const PLUTO_CSS_IMPORT = '@plutocms/css'
|
|
|
11
11
|
/** The opt-in Pluto site theme: an alias of `#build/pluto-site.css`. */
|
|
12
12
|
export const PLUTO_SITE_CSS_IMPORT = '@plutocms/css/site'
|
|
13
13
|
|
|
14
|
+
/** A layer's plain CSS for its public components. See the public-components skill. */
|
|
15
|
+
export const LAYER_BASE_CSS_FILE = 'assets/css/pluto-base.css'
|
|
16
|
+
|
|
17
|
+
/** The opt-in look of the public components: an alias of `#build/pluto-base.css`. */
|
|
18
|
+
export const PLUTO_BASE_CSS_IMPORT = '@plutocms/css/base'
|
|
19
|
+
|
|
14
20
|
function toPosix(path: string): string {
|
|
15
21
|
return path.replaceAll('\\', '/')
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
|
-
* Returns the `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* later in the build and wins.
|
|
25
|
+
* Returns the `file` of every layer that has one, base layer first. Nuxt
|
|
26
|
+
* lists the layers top first (the app, then each layer it extends), so the
|
|
27
|
+
* list is reversed: a higher layer's rules then come later and win.
|
|
23
28
|
*/
|
|
24
29
|
export function findLayerCssFiles(
|
|
25
30
|
layerAppDirs: string[],
|
|
26
|
-
exists: (path: string) => boolean
|
|
31
|
+
exists: (path: string) => boolean,
|
|
32
|
+
file = LAYER_CSS_FILE
|
|
27
33
|
): string[] {
|
|
28
34
|
return [...layerAppDirs]
|
|
29
35
|
.reverse()
|
|
30
|
-
.map((appDir) => toPosix(join(appDir,
|
|
31
|
-
.filter((
|
|
36
|
+
.map((appDir) => toPosix(join(appDir, file)))
|
|
37
|
+
.filter((path) => exists(path))
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
@@ -37,9 +43,26 @@ export function findLayerCssFiles(
|
|
|
37
43
|
* another drive (Windows) stays absolute.
|
|
38
44
|
*/
|
|
39
45
|
export function renderPlutoCss(files: string[], buildDir: string): string {
|
|
46
|
+
return renderLayerImports(files, buildDir, LAYER_CSS_FILE)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Renders `#build/pluto-base.css`: one `@import` for each layer's
|
|
51
|
+
* `pluto-base.css`. The plain CSS of the public components: an app imports
|
|
52
|
+
* it (`@plutocms/css/base`) with or without Tailwind.
|
|
53
|
+
*/
|
|
54
|
+
export function renderPlutoBaseCss(files: string[], buildDir: string): string {
|
|
55
|
+
return renderLayerImports(files, buildDir, LAYER_BASE_CSS_FILE)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderLayerImports(
|
|
59
|
+
files: string[],
|
|
60
|
+
buildDir: string,
|
|
61
|
+
name: string
|
|
62
|
+
): string {
|
|
40
63
|
return [
|
|
41
64
|
'/* Generated by @plutocms/core (modules/pluto-css). Do not edit. */',
|
|
42
|
-
|
|
65
|
+
`/* Each Pluto layer's app/${name}, base layer first. */`,
|
|
43
66
|
...files.map((file) => cssImport(file, buildDir)),
|
|
44
67
|
'',
|
|
45
68
|
].join('\n')
|
|
@@ -70,14 +93,19 @@ function cssImport(file: string, buildDir: string): string {
|
|
|
70
93
|
return `@import "${path}";`
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Renders `#build/pluto-admin.css`: the Tailwind build of the admin. The module
|
|
98
|
+
* scopes its output to the admin (see scope.ts), and PlutoAdminTheme imports it.
|
|
99
|
+
*/
|
|
100
|
+
export function renderPlutoAdminCss(): string {
|
|
75
101
|
return [
|
|
76
102
|
'/* Generated by @plutocms/core (modules/pluto-css). Do not edit. */',
|
|
77
|
-
'/*
|
|
103
|
+
'/* The admin build: scoped to [data-pluto-admin] and [data-pluto-admin-scope]. */',
|
|
78
104
|
'@import "tailwindcss";',
|
|
79
105
|
'@import "@nuxt/ui";',
|
|
80
106
|
`@import "${PLUTO_CSS_IMPORT}";`,
|
|
107
|
+
// The admin shows public components too: the post preview, the 3D preview.
|
|
108
|
+
`@import "${PLUTO_BASE_CSS_IMPORT}";`,
|
|
81
109
|
'',
|
|
82
110
|
].join('\n')
|
|
83
111
|
}
|
|
@@ -111,17 +139,12 @@ export interface CssEntryInfo {
|
|
|
111
139
|
file: string
|
|
112
140
|
/** It imports Tailwind (`@import 'tailwindcss'`), so it is a Tailwind build. */
|
|
113
141
|
tailwind: boolean
|
|
114
|
-
/** It imports `@nuxt/ui`. */
|
|
115
|
-
nuxtUi: boolean
|
|
116
|
-
/** It imports `@plutocms/css` (or the generated `#build/pluto.css` itself). */
|
|
117
|
-
plutoCss: boolean
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
/**
|
|
121
145
|
* Follows the local `@import`s of a CSS entry, to `maxDepth`, and reports
|
|
122
|
-
*
|
|
123
|
-
* returns `undefined` for a package import
|
|
124
|
-
* not followed.
|
|
146
|
+
* whether it imports Tailwind. `resolveImport` turns a specifier into a file
|
|
147
|
+
* path, or returns `undefined` for a package import, which is not followed.
|
|
125
148
|
*/
|
|
126
149
|
export function analyzeCssEntry(
|
|
127
150
|
file: string,
|
|
@@ -129,12 +152,7 @@ export function analyzeCssEntry(
|
|
|
129
152
|
resolveImport: (specifier: string, fromFile: string) => string | undefined,
|
|
130
153
|
maxDepth = 3
|
|
131
154
|
): CssEntryInfo {
|
|
132
|
-
const info: CssEntryInfo = {
|
|
133
|
-
file,
|
|
134
|
-
tailwind: false,
|
|
135
|
-
nuxtUi: false,
|
|
136
|
-
plutoCss: false,
|
|
137
|
-
}
|
|
155
|
+
const info: CssEntryInfo = { file, tailwind: false }
|
|
138
156
|
const seen = new Set<string>()
|
|
139
157
|
|
|
140
158
|
function visit(path: string, depth: number) {
|
|
@@ -149,13 +167,6 @@ export function analyzeCssEntry(
|
|
|
149
167
|
for (const specifier of parseCssImports(css)) {
|
|
150
168
|
if (specifier === 'tailwindcss') {
|
|
151
169
|
info.tailwind = true
|
|
152
|
-
} else if (specifier === '@nuxt/ui') {
|
|
153
|
-
info.nuxtUi = true
|
|
154
|
-
} else if (
|
|
155
|
-
specifier === PLUTO_CSS_IMPORT ||
|
|
156
|
-
(/(?:^|\/)pluto\.css$/.test(specifier) && specifier.includes('#build'))
|
|
157
|
-
) {
|
|
158
|
-
info.plutoCss = true
|
|
159
170
|
} else {
|
|
160
171
|
const next = resolveImport(specifier, path)
|
|
161
172
|
if (next) {
|
|
@@ -179,27 +190,18 @@ export function resolveRelativeImport(
|
|
|
179
190
|
: undefined
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
export interface CssSetupReport {
|
|
183
|
-
/** Inject the fallback entry: no CSS entry imports Tailwind. */
|
|
184
|
-
injectFallback: boolean
|
|
185
|
-
warnings: string[]
|
|
186
|
-
}
|
|
187
|
-
|
|
188
193
|
/**
|
|
189
|
-
* Checks the app's CSS
|
|
190
|
-
*
|
|
194
|
+
* Checks the app's CSS entries and returns the warnings. The admin has a build
|
|
195
|
+
* of its own, so the app's entry is for the site only, and an app with no
|
|
196
|
+
* Tailwind entry is fine. A Tailwind build belongs in the app, not in a layer,
|
|
197
|
+
* and there is one. `layerRoots` are the root dirs of every layer except the app.
|
|
191
198
|
*/
|
|
192
199
|
export function checkCssSetup(
|
|
193
200
|
entries: CssEntryInfo[],
|
|
194
201
|
layerRoots: string[]
|
|
195
|
-
):
|
|
202
|
+
): string[] {
|
|
196
203
|
const warnings: string[] = []
|
|
197
204
|
const tailwindEntries = entries.filter((entry) => entry.tailwind)
|
|
198
|
-
const snippet = ` @import 'tailwindcss';\n @import '@nuxt/ui';\n @import '${PLUTO_CSS_IMPORT}';`
|
|
199
|
-
|
|
200
|
-
if (tailwindEntries.length === 0) {
|
|
201
|
-
return { injectFallback: true, warnings }
|
|
202
|
-
}
|
|
203
205
|
|
|
204
206
|
for (const entry of tailwindEntries) {
|
|
205
207
|
const layerRoot = layerRoots.find((root) =>
|
|
@@ -218,17 +220,5 @@ export function checkCssSetup(
|
|
|
218
220
|
)
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
|
|
222
|
-
warnings.push(
|
|
223
|
-
`No CSS entry imports ${PLUTO_CSS_IMPORT}. The admin theme and layer tokens are missing. Start the app's CSS entry with:\n${snippet}`
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!tailwindEntries.some((entry) => entry.nuxtUi)) {
|
|
228
|
-
warnings.push(
|
|
229
|
-
`No CSS entry imports @nuxt/ui. Nuxt UI styles, the \`light:\` variant, and the layer sources are missing. Start the app's CSS entry with:\n${snippet}`
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return { injectFallback: false, warnings }
|
|
223
|
+
return warnings
|
|
234
224
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import {
|
|
5
5
|
addTemplate,
|
|
6
|
+
addVitePlugin,
|
|
6
7
|
defineNuxtModule,
|
|
7
8
|
getLayerDirectories,
|
|
8
9
|
resolvePath,
|
|
@@ -12,39 +13,52 @@ import {
|
|
|
12
13
|
analyzeCssEntry,
|
|
13
14
|
checkCssSetup,
|
|
14
15
|
findLayerCssFiles,
|
|
16
|
+
LAYER_BASE_CSS_FILE,
|
|
17
|
+
PLUTO_BASE_CSS_IMPORT,
|
|
15
18
|
PLUTO_CSS_IMPORT,
|
|
16
19
|
PLUTO_SITE_CSS_IMPORT,
|
|
20
|
+
renderPlutoAdminCss,
|
|
21
|
+
renderPlutoBaseCss,
|
|
17
22
|
renderPlutoCss,
|
|
18
|
-
renderPlutoEntryCss,
|
|
19
23
|
renderPlutoSiteCss,
|
|
20
24
|
resolveRelativeImport,
|
|
21
25
|
} from './css-entries'
|
|
26
|
+
import { scopeCss } from './scope'
|
|
22
27
|
|
|
23
28
|
export interface PlutoCssOptions {
|
|
24
|
-
/** Warn at build time about a
|
|
29
|
+
/** Warn at build time about a Tailwind build in a layer, or more than one. Default true. */
|
|
25
30
|
check?: boolean
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
34
|
+
* The CSS of Pluto. The admin and the site have separate Tailwind builds:
|
|
30
35
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
36
|
+
* - The admin build `#build/pluto-admin.css` (`tailwindcss`, `@nuxt/ui`,
|
|
37
|
+
* `@plutocms/css`). Tailwind compiles it at the app's build, then the
|
|
38
|
+
* `pluto-css:scope-admin` plugin scopes every rule to the admin
|
|
39
|
+
* (`[data-pluto-admin]`, `[data-pluto-admin-scope]`). PlutoAdminTheme imports
|
|
40
|
+
* it, so it loads only with the admin UI.
|
|
41
|
+
* - The site's build is the app's own CSS entry, if the app has one. A site
|
|
42
|
+
* needs no Tailwind. A Tailwind site imports what its pages use:
|
|
43
|
+
*
|
|
44
|
+
* @import 'tailwindcss';
|
|
45
|
+
* @import '@nuxt/ui';
|
|
46
|
+
* @import '@plutocms/css';
|
|
34
47
|
*
|
|
35
48
|
* - `@plutocms/css` is an alias of the generated `#build/pluto.css`. It imports
|
|
36
|
-
* each layer's app/assets/css/pluto-layer.css (
|
|
37
|
-
*
|
|
38
|
-
*
|
|
49
|
+
* each layer's app/assets/css/pluto-layer.css (the Pluto tokens, the layer
|
|
50
|
+
* tokens). Nuxt UI already adds `@source` for every layer's app/ dir, so this
|
|
51
|
+
* file adds no sources.
|
|
39
52
|
* - `@plutocms/css/site` (opt-in) is an alias of `#build/pluto-site.css`: the
|
|
40
53
|
* Pluto site theme of core (app/assets/css/pluto-site-theme.css), the look of
|
|
41
54
|
* the Pluto docs for a site.
|
|
55
|
+
* - `@plutocms/css/base` (opt-in) is an alias of `#build/pluto-base.css`: each
|
|
56
|
+
* layer's app/assets/css/pluto-base.css, the plain CSS look of its public
|
|
57
|
+
* components. It needs no Tailwind. The admin build imports it too.
|
|
42
58
|
* - The names are aliases, not npm packages, so they resolve with every package
|
|
43
59
|
* manager, also when the app installs only an outer layer.
|
|
44
|
-
* -
|
|
45
|
-
*
|
|
46
|
-
* - The module warns about a layer that registers its own Tailwind build,
|
|
47
|
-
* more than one build, or a missing import.
|
|
60
|
+
* - The module warns about a layer that registers its own Tailwind build, and
|
|
61
|
+
* about more than one build in the app.
|
|
48
62
|
*
|
|
49
63
|
* See the single-tailwind-build skill.
|
|
50
64
|
*/
|
|
@@ -76,18 +90,96 @@ export default defineNuxtModule<PlutoCssOptions>({
|
|
|
76
90
|
getContents: () => renderPlutoSiteCss(siteTheme, nuxt.options.buildDir),
|
|
77
91
|
})
|
|
78
92
|
|
|
79
|
-
// The
|
|
80
|
-
//
|
|
81
|
-
|
|
93
|
+
// The opt-in plain CSS of the public components, from every layer. An
|
|
94
|
+
// app imports `@plutocms/css/base`, with or without Tailwind.
|
|
95
|
+
const layerBaseCssFiles = findLayerCssFiles(
|
|
96
|
+
layers.map((layer) => layer.app),
|
|
97
|
+
existsSync,
|
|
98
|
+
LAYER_BASE_CSS_FILE
|
|
99
|
+
)
|
|
100
|
+
addTemplate({
|
|
101
|
+
filename: 'pluto-base.css',
|
|
102
|
+
write: true,
|
|
103
|
+
getContents: () =>
|
|
104
|
+
renderPlutoBaseCss(layerBaseCssFiles, nuxt.options.buildDir),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// The names that an app's CSS entry imports. Tailwind and Vite resolve
|
|
108
|
+
// `@import` through the aliases. `/site` and `/base` come first: an alias
|
|
109
|
+
// also matches the paths under its name, so `@plutocms/css` would take them.
|
|
82
110
|
nuxt.options.alias[PLUTO_SITE_CSS_IMPORT] = join(
|
|
83
111
|
nuxt.options.buildDir,
|
|
84
112
|
'pluto-site.css'
|
|
85
113
|
)
|
|
114
|
+
nuxt.options.alias[PLUTO_BASE_CSS_IMPORT] = join(
|
|
115
|
+
nuxt.options.buildDir,
|
|
116
|
+
'pluto-base.css'
|
|
117
|
+
)
|
|
86
118
|
nuxt.options.alias[PLUTO_CSS_IMPORT] = join(
|
|
87
119
|
nuxt.options.buildDir,
|
|
88
120
|
'pluto.css'
|
|
89
121
|
)
|
|
90
122
|
|
|
123
|
+
// The admin build. Not written to disk: on disk, Tailwind IntelliSense
|
|
124
|
+
// could take this entry for the app's design system. Not in `css`:
|
|
125
|
+
// PlutoAdminTheme imports it, so it loads with the admin UI only.
|
|
126
|
+
addTemplate({
|
|
127
|
+
filename: 'pluto-admin.css',
|
|
128
|
+
getContents: renderPlutoAdminCss,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Server HTML links the admin build, not inlines it: inline, every page
|
|
132
|
+
// view of a staff user would carry the full build, which no cache keeps.
|
|
133
|
+
const inline = nuxt.options.features.inlineStyles
|
|
134
|
+
if (inline) {
|
|
135
|
+
nuxt.options.features.inlineStyles = (id) =>
|
|
136
|
+
!id?.includes('PlutoAdminTheme.vue') && (inline === true || inline(id))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Nuxt prefetches the CSS of every async chunk, so each visitor would
|
|
140
|
+
// download the admin build after the page loads. The plugin below finds
|
|
141
|
+
// the CSS files of the client chunk with the build, and this hook turns
|
|
142
|
+
// off their prefetch. The chunks that render admin UI still load them.
|
|
143
|
+
// The file names, without the assets dir: the manifest keys have none.
|
|
144
|
+
const adminCssFiles = new Set<string>()
|
|
145
|
+
nuxt.hook('build:manifest', (manifest) => {
|
|
146
|
+
for (const file of adminCssFiles) {
|
|
147
|
+
if (manifest[file]) {
|
|
148
|
+
manifest[file].prefetch = false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// A file path, or a Nuxt virtual id (`virtual:nuxt:…%2Fpluto-admin.css`).
|
|
154
|
+
const adminCssId = /(?:[\\/]|%2F)pluto-admin\.css(?:\?|$)/
|
|
155
|
+
addVitePlugin({
|
|
156
|
+
name: 'pluto-css:scope-admin',
|
|
157
|
+
transform(code, id) {
|
|
158
|
+
if (adminCssId.test(id)) {
|
|
159
|
+
return { code: scopeCss(code), map: null }
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
generateBundle(_options, bundle) {
|
|
163
|
+
if (this.environment?.name !== 'client') {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
for (const chunk of Object.values(bundle)) {
|
|
167
|
+
if (
|
|
168
|
+
chunk.type === 'chunk' &&
|
|
169
|
+
chunk.moduleIds.some((id) => adminCssId.test(id))
|
|
170
|
+
) {
|
|
171
|
+
for (const file of chunk.viteMetadata?.importedCss ?? []) {
|
|
172
|
+
adminCssFiles.add(file.slice(file.lastIndexOf('/') + 1))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (!options.check || nuxt.options._prepare) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
91
183
|
const entries = []
|
|
92
184
|
for (const entry of nuxt.options.css) {
|
|
93
185
|
const file =
|
|
@@ -112,29 +204,8 @@ export default defineNuxtModule<PlutoCssOptions>({
|
|
|
112
204
|
const layerRoots = layers
|
|
113
205
|
.map((layer) => layer.root.replaceAll('\\', '/'))
|
|
114
206
|
.filter((root) => root !== appRoot)
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
// Only an app with no Tailwind entry gets the fallback file. In an app with
|
|
118
|
-
// its own entry, the file would be a second Tailwind entry on disk, and
|
|
119
|
-
// Tailwind IntelliSense could take it for the app's design system.
|
|
120
|
-
if (report.injectFallback) {
|
|
121
|
-
addTemplate({
|
|
122
|
-
filename: 'pluto-entry.css',
|
|
123
|
-
write: true,
|
|
124
|
-
getContents: renderPlutoEntryCss,
|
|
125
|
-
})
|
|
126
|
-
nuxt.options.css.push('#build/pluto-entry.css')
|
|
127
|
-
if (!nuxt.options._prepare) {
|
|
128
|
-
logger.info(
|
|
129
|
-
'No CSS entry imports Tailwind. Using the Pluto fallback entry (#build/pluto-entry.css).'
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (options.check && !nuxt.options._prepare) {
|
|
135
|
-
for (const warning of report.warnings) {
|
|
136
|
-
logger.warn(warning)
|
|
137
|
-
}
|
|
207
|
+
for (const warning of checkCssSetup(entries, layerRoots)) {
|
|
208
|
+
logger.warn(warning)
|
|
138
209
|
}
|
|
139
210
|
},
|
|
140
211
|
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Scopes a compiled stylesheet to the Pluto admin: every rule matches only
|
|
2
|
+
// inside a scope root. Pure (no `nuxt/kit`), so the unit tests call it directly.
|
|
3
|
+
// See the single-tailwind-build skill, "The admin build".
|
|
4
|
+
import type { AtRule, Rule } from 'postcss'
|
|
5
|
+
import postcss from 'postcss'
|
|
6
|
+
import selectorParser from 'postcss-selector-parser'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The elements that start the admin scope: <html> on an admin page
|
|
10
|
+
* (`data-pluto-admin`), and the admin bar and its portals on a public page
|
|
11
|
+
* (`data-pluto-admin-scope`).
|
|
12
|
+
*/
|
|
13
|
+
export const PLUTO_ADMIN_SCOPE_ROOTS = [
|
|
14
|
+
'[data-pluto-admin]',
|
|
15
|
+
'[data-pluto-admin-scope]',
|
|
16
|
+
] as const
|
|
17
|
+
|
|
18
|
+
/** Selectors that mean the document root. They become the scope roots. */
|
|
19
|
+
const ROOT_SELECTORS = new Set([':root', ':host', 'html'])
|
|
20
|
+
|
|
21
|
+
/** A pseudo-element: the scope goes before it. */
|
|
22
|
+
const PSEUDO_ELEMENT = /^(?:::|:(?:before|after|first-line|first-letter)$)/
|
|
23
|
+
|
|
24
|
+
/** Scopes each selector of a selector list. */
|
|
25
|
+
function scopeSelectorList(selector: string, roots: readonly string[]): string {
|
|
26
|
+
// An element in the scope: a root itself, or anything inside a root.
|
|
27
|
+
const inScope = `:is(${[...roots, ...roots.map((root) => `${root} *`)].join(', ')})`
|
|
28
|
+
const rootNames = roots.map((root) => root.slice(1, -1))
|
|
29
|
+
const out: string[] = []
|
|
30
|
+
|
|
31
|
+
selectorParser((list) => {
|
|
32
|
+
list.each((sel) => {
|
|
33
|
+
const text = sel.toString().trim()
|
|
34
|
+
if (rootNames.some((name) => text.includes(name))) {
|
|
35
|
+
out.push(text)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (ROOT_SELECTORS.has(text)) {
|
|
39
|
+
// A doubled root, (0,2,0): on <html> of an admin page it beats the
|
|
40
|
+
// app's own `:root` (0,1,0), whatever order the two files load in.
|
|
41
|
+
out.push(...roots.map((root) => `${root}${root}`))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
// The subject is the last compound: `.a .b::before` becomes
|
|
45
|
+
// `.a .b:is(…)::before`.
|
|
46
|
+
const nodes = sel.nodes
|
|
47
|
+
let start = 0
|
|
48
|
+
nodes.forEach((node, index) => {
|
|
49
|
+
if (node.type === 'combinator') {
|
|
50
|
+
start = index + 1
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
const pseudoElement = nodes.findIndex(
|
|
54
|
+
(node, index) =>
|
|
55
|
+
index >= start &&
|
|
56
|
+
node.type === 'pseudo' &&
|
|
57
|
+
PSEUDO_ELEMENT.test(node.value)
|
|
58
|
+
)
|
|
59
|
+
const scope = selectorParser.pseudo({ value: inScope })
|
|
60
|
+
if (pseudoElement === -1) {
|
|
61
|
+
sel.append(scope)
|
|
62
|
+
} else {
|
|
63
|
+
// The space before the pseudo-element (`, ::after`) moves to the
|
|
64
|
+
// scope. Left in place, it would be a descendant combinator.
|
|
65
|
+
const target = nodes[pseudoElement]!
|
|
66
|
+
scope.spaces.before = target.spaces.before
|
|
67
|
+
target.spaces.before = ''
|
|
68
|
+
sel.insertBefore(target, scope)
|
|
69
|
+
}
|
|
70
|
+
out.push(sel.toString().trim())
|
|
71
|
+
})
|
|
72
|
+
}).processSync(selector)
|
|
73
|
+
|
|
74
|
+
return out.join(', ')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True when the rule is in `@keyframes`, or nested in another rule. */
|
|
78
|
+
function keepAsIs(rule: Rule): boolean {
|
|
79
|
+
let parent = rule.parent
|
|
80
|
+
while (parent && parent.type !== 'root') {
|
|
81
|
+
if (parent.type === 'rule') {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
parent.type === 'atrule' &&
|
|
86
|
+
/keyframes$/i.test((parent as AtRule).name)
|
|
87
|
+
) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
parent = parent.parent
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns `css` with every top-level rule scoped to `roots`:
|
|
97
|
+
*
|
|
98
|
+
* - `:root`, `:host`, and `html` become the roots, doubled for (0,2,0), so the
|
|
99
|
+
* theme variables and the base of the document apply from each root down,
|
|
100
|
+
* over the app's own `:root` rules.
|
|
101
|
+
* - Any other selector gets `:is(<root>, <root> *)` on its subject. It then
|
|
102
|
+
* matches only in the scope, with (0,1,0) more specificity.
|
|
103
|
+
* - A selector that names a root already stays.
|
|
104
|
+
* - Rules nested in rules (they follow their parent) and `@keyframes` stay.
|
|
105
|
+
*/
|
|
106
|
+
export function scopeCss(
|
|
107
|
+
css: string,
|
|
108
|
+
roots: readonly string[] = PLUTO_ADMIN_SCOPE_ROOTS
|
|
109
|
+
): string {
|
|
110
|
+
const ast = postcss.parse(css)
|
|
111
|
+
ast.walkRules((rule) => {
|
|
112
|
+
if (!keepAsIs(rule)) {
|
|
113
|
+
rule.selector = scopeSelectorList(rule.selector, roots)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
return ast.toString()
|
|
117
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -41,9 +41,10 @@ export default defineNuxtConfig({
|
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
|
|
44
|
-
// No `css` entry:
|
|
45
|
-
//
|
|
46
|
-
// @plutocms/css (#build/pluto.css). See the
|
|
44
|
+
// No `css` entry: modules/pluto-css gives the admin a Tailwind build of its
|
|
45
|
+
// own, and the site's build is the app's CSS entry. Both get this layer's
|
|
46
|
+
// pluto-layer.css through @plutocms/css (#build/pluto.css). See the
|
|
47
|
+
// single-tailwind-build skill.
|
|
47
48
|
|
|
48
49
|
runtimeConfig: {
|
|
49
50
|
// Private mail settings. Empty defaults on purpose: Nuxt reads
|
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.47.0",
|
|
5
5
|
"description": "The base layer every Pluto app extends: the admin, the content model, and the Git CMS",
|
|
6
6
|
"author": "Victor Ribeiro",
|
|
7
7
|
"license": "MIT",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"@vueuse/nuxt": "^15.0.0",
|
|
48
48
|
"image-meta": "^0.3.0",
|
|
49
49
|
"marked": "^18.1.1",
|
|
50
|
+
"postcss": "^8.5.30",
|
|
51
|
+
"postcss-selector-parser": "^7.1.4",
|
|
50
52
|
"reka-ui": "^2.11.0",
|
|
51
53
|
"tailwind-merge": "^3.7.0",
|
|
52
54
|
"ultrahtml": "^1.7.0",
|